dsh-workbuddy-xdpool 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.en.md +11 -0
- package/README.md +11 -0
- package/assets/region-tabs.png +0 -0
- package/lib/bin.js +1335 -1294
- package/lib/client.js +604 -219
- package/lib/index.d.ts +716 -629
- package/lib/index.js +2139 -1920
- package/package.json +109 -109
- package/screenshots.json +1 -0
package/lib/bin.js
CHANGED
|
@@ -1,1294 +1,1335 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir, platform } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
import z from "@deepseek-ai/schemastery";
|
|
6
|
-
import "@earendil-works/pi-ai";
|
|
7
|
-
import "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
8
|
-
import "@deepseek-ai/dsh-llm";
|
|
9
|
-
import "@deepseek-ai/dsh-llm-pi-ai";
|
|
10
|
-
//#region src/
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
function
|
|
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
|
-
const
|
|
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
|
-
if (
|
|
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
|
-
if (
|
|
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
|
-
const
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
"
|
|
532
|
-
|
|
533
|
-
...
|
|
534
|
-
...
|
|
535
|
-
...
|
|
536
|
-
"
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
return
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
function
|
|
551
|
-
const
|
|
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
|
-
return
|
|
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
|
-
return
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
const
|
|
764
|
-
if (
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
for (const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
/**
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
return
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
lines.push(
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
console.
|
|
1291
|
-
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
import "@earendil-works/pi-ai";
|
|
7
|
+
import "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
8
|
+
import "@deepseek-ai/dsh-llm";
|
|
9
|
+
import "@deepseek-ai/dsh-llm-pi-ai";
|
|
10
|
+
//#region src/upstream.ts
|
|
11
|
+
/** CN chat base per dingminhua's on-machine probe (HTTP 200 for chat/models). */
|
|
12
|
+
const CN_CHAT_BASE = "https://copilot.tencent.com";
|
|
13
|
+
/** Billing/console base — `www.codebuddy.cn` is the billing origin. */
|
|
14
|
+
const CN_BILLING_BASE = "https://www.codebuddy.cn";
|
|
15
|
+
/** Global base for `workbuddy.ai` logins. */
|
|
16
|
+
const GLOBAL_BASE = "https://www.workbuddy.ai";
|
|
17
|
+
/** Client UA the desktop CLI uses. */
|
|
18
|
+
const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
|
|
19
|
+
const JSON_TIMEOUT_MS = 3e4;
|
|
20
|
+
const ERROR_BODY_LIMIT = 4096;
|
|
21
|
+
/** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
|
|
22
|
+
const HARD_CREDIT_MARKERS = [
|
|
23
|
+
"insufficient credit",
|
|
24
|
+
"no credit",
|
|
25
|
+
"credit exhausted",
|
|
26
|
+
"out of credit",
|
|
27
|
+
"quota exceeded",
|
|
28
|
+
"quota exhaust",
|
|
29
|
+
"payment required",
|
|
30
|
+
"credit not enough",
|
|
31
|
+
"not enough credit",
|
|
32
|
+
"积分不足",
|
|
33
|
+
"额度不足",
|
|
34
|
+
"余额不足",
|
|
35
|
+
"积分用完",
|
|
36
|
+
"额度用尽",
|
|
37
|
+
"没有积分"
|
|
38
|
+
];
|
|
39
|
+
/** Session-invalidation markers that mean "sign in again in the WorkBuddy app". */
|
|
40
|
+
const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
|
|
41
|
+
function regionOf(domain) {
|
|
42
|
+
const lowered = domain.trim().toLowerCase();
|
|
43
|
+
if (lowered.endsWith(".workbuddy.ai") || lowered.endsWith(".workbuddy.cc")) return "global";
|
|
44
|
+
if (lowered === "workbuddy.ai" || lowered === "workbuddy.cc") return "global";
|
|
45
|
+
return "cn";
|
|
46
|
+
}
|
|
47
|
+
function chatBase(credential) {
|
|
48
|
+
return regionOf(credential.domain) === "global" ? GLOBAL_BASE : CN_CHAT_BASE;
|
|
49
|
+
}
|
|
50
|
+
function billingBase(credential) {
|
|
51
|
+
return regionOf(credential.domain) === "global" ? GLOBAL_BASE : CN_BILLING_BASE;
|
|
52
|
+
}
|
|
53
|
+
function originReferer(credential) {
|
|
54
|
+
return regionOf(credential.domain) === "global" ? GLOBAL_BASE : CN_BILLING_BASE;
|
|
55
|
+
}
|
|
56
|
+
/** Headers every upstream request shares. */
|
|
57
|
+
function commonHeaders(credential) {
|
|
58
|
+
return {
|
|
59
|
+
"Accept": "application/json, text/plain, */*",
|
|
60
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
61
|
+
"Origin": originReferer(credential),
|
|
62
|
+
"Referer": `${originReferer(credential)}/`,
|
|
63
|
+
"User-Agent": CLIENT_UA
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Chat request headers, including the X-No-* conventions the official CLI uses. */
|
|
67
|
+
function chatHeaders(credential) {
|
|
68
|
+
return {
|
|
69
|
+
...commonHeaders(credential),
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"Authorization": `Bearer ${credential.accessToken}`,
|
|
72
|
+
...credential.uid === "" || credential.uid === void 0 ? { "X-No-User-Id": "1" } : { "X-User-Id": credential.uid },
|
|
73
|
+
...credential.enterpriseId === void 0 || credential.enterpriseId === "" ? { "X-No-Enterprise-Id": "1" } : { "X-Enterprise-Id": credential.enterpriseId },
|
|
74
|
+
...credential.domain === "" ? { "X-No-Department-Info": "1" } : { "X-Domain": credential.domain },
|
|
75
|
+
"X-Product": "SaaS"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
|
|
79
|
+
function refreshHeaders(credential) {
|
|
80
|
+
const headers = {
|
|
81
|
+
...commonHeaders(credential),
|
|
82
|
+
"X-Refresh-Token": credential.refreshToken,
|
|
83
|
+
"X-Auth-Refresh-Source": "workbuddy"
|
|
84
|
+
};
|
|
85
|
+
if (credential.enterpriseId !== void 0 && credential.enterpriseId !== "") headers["X-Enterprise-Id"] = credential.enterpriseId;
|
|
86
|
+
return headers;
|
|
87
|
+
}
|
|
88
|
+
/** Billing request headers. */
|
|
89
|
+
function billingHeaders(credential) {
|
|
90
|
+
const headers = {
|
|
91
|
+
"Authorization": `Bearer ${credential.accessToken}`,
|
|
92
|
+
"Accept": "application/json",
|
|
93
|
+
"Content-Type": "application/json"
|
|
94
|
+
};
|
|
95
|
+
if (credential.uid !== "" && credential.uid !== void 0) headers["X-User-Id"] = credential.uid;
|
|
96
|
+
if (credential.enterpriseId !== void 0 && credential.enterpriseId !== "") {
|
|
97
|
+
headers["X-Enterprise-Id"] = credential.enterpriseId;
|
|
98
|
+
headers["X-Tenant-Id"] = credential.enterpriseId;
|
|
99
|
+
}
|
|
100
|
+
if (credential.domain !== "") headers["X-Domain"] = credential.domain;
|
|
101
|
+
return headers;
|
|
102
|
+
}
|
|
103
|
+
async function readEnvelope(response) {
|
|
104
|
+
const text = await response.text();
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(text);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error(`workbuddy upstream returned non-JSON (http ${response.status}): ${text.slice(0, 160)}`);
|
|
110
|
+
}
|
|
111
|
+
if (typeof parsed !== "object" || parsed === null) throw new Error(`workbuddy upstream returned an unexpected document (http ${response.status})`);
|
|
112
|
+
const document = parsed;
|
|
113
|
+
return {
|
|
114
|
+
code: typeof document["code"] === "number" ? document["code"] : 0,
|
|
115
|
+
msg: typeof document["msg"] === "string" ? document["msg"] : "",
|
|
116
|
+
data: "data" in document ? document["data"] : void 0
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Fail an envelope whose business code is non-zero, classified like HTTP errors. */
|
|
120
|
+
function envelopeError(status, envelope) {
|
|
121
|
+
const kind = classifyUpstreamError(status, envelope.msg);
|
|
122
|
+
return /* @__PURE__ */ new Error(`workbuddy upstream ${kind} (http ${status}): ${envelope.msg.slice(0, 160)}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Classify an upstream failure from its HTTP status and body excerpt.
|
|
126
|
+
* Body markers win over status, because the upstream reuses 400/200 for
|
|
127
|
+
* several distinct conditions.
|
|
128
|
+
*/
|
|
129
|
+
function classifyUpstreamError(status, body) {
|
|
130
|
+
if (status === 402) return "hard_credit";
|
|
131
|
+
const lower = body.toLowerCase();
|
|
132
|
+
for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
|
|
133
|
+
for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
|
|
134
|
+
if (status === 429) return "soft_rate";
|
|
135
|
+
if (body.includes("soft_rate") || body.includes("\"code\":6004") || body.includes("频率限制")) return "soft_rate";
|
|
136
|
+
if (status === 404) return "not_found";
|
|
137
|
+
if (status >= 500) return "server";
|
|
138
|
+
return "client";
|
|
139
|
+
}
|
|
140
|
+
/** Parse the upstream's `credits` string into a multiplier. */
|
|
141
|
+
function parseCreditMultiplier(value) {
|
|
142
|
+
if (typeof value !== "string") return void 0;
|
|
143
|
+
const match = /x\s*([0-9]*\.?[0-9]+)/iu.exec(value);
|
|
144
|
+
if (match === null) return void 0;
|
|
145
|
+
const parsed = Number(match[1]);
|
|
146
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
147
|
+
}
|
|
148
|
+
/** Parse the upstream's `reasoning` object; unknown shapes degrade to `{}`. */
|
|
149
|
+
function parseReasoning(value) {
|
|
150
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
151
|
+
const raw = value;
|
|
152
|
+
const supportedEfforts = Array.isArray(raw["supportedEfforts"]) ? raw["supportedEfforts"].filter((effort) => typeof effort === "string") : void 0;
|
|
153
|
+
const defaultEffort = typeof raw["defaultEffort"] === "string" ? raw["defaultEffort"] : void 0;
|
|
154
|
+
const canDisableThinking = typeof raw["canDisableThinking"] === "boolean" ? raw["canDisableThinking"] : void 0;
|
|
155
|
+
if (supportedEfforts === void 0 && defaultEffort === void 0 && canDisableThinking === void 0) return;
|
|
156
|
+
return {
|
|
157
|
+
...supportedEfforts === void 0 || supportedEfforts.length === 0 ? {} : { supportedEfforts },
|
|
158
|
+
...defaultEffort === void 0 ? {} : { defaultEffort },
|
|
159
|
+
...canDisableThinking === void 0 ? {} : { canDisableThinking }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Parse one catalog entry; entries without usable token limits are dropped. */
|
|
163
|
+
function parseUpstreamModel(value) {
|
|
164
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
165
|
+
const raw = value;
|
|
166
|
+
const id = typeof raw["id"] === "string" ? raw["id"] : "";
|
|
167
|
+
if (id === "" || raw["disabled"] === true) return void 0;
|
|
168
|
+
const input = typeof raw["maxInputTokens"] === "number" ? raw["maxInputTokens"] : 0;
|
|
169
|
+
const output = typeof raw["maxOutputTokens"] === "number" ? raw["maxOutputTokens"] : 0;
|
|
170
|
+
if (input <= 0 || output <= 0) return void 0;
|
|
171
|
+
const name = typeof raw["name"] === "string" && raw["name"] !== "" ? raw["name"] : id;
|
|
172
|
+
const descriptionZh = typeof raw["descriptionZh"] === "string" && raw["descriptionZh"] !== "" ? raw["descriptionZh"] : void 0;
|
|
173
|
+
const descriptionEn = typeof raw["descriptionEn"] === "string" && raw["descriptionEn"] !== "" ? raw["descriptionEn"] : void 0;
|
|
174
|
+
const creditMultiplier = parseCreditMultiplier(raw["credits"]);
|
|
175
|
+
const reasoning = parseReasoning(raw["reasoning"]);
|
|
176
|
+
const supportsToolCall = typeof raw["supportsToolCall"] === "boolean" ? raw["supportsToolCall"] : void 0;
|
|
177
|
+
return {
|
|
178
|
+
id,
|
|
179
|
+
name,
|
|
180
|
+
contextWindow: input,
|
|
181
|
+
maxTokens: output,
|
|
182
|
+
...creditMultiplier === void 0 ? {} : { creditMultiplier },
|
|
183
|
+
...reasoning === void 0 ? {} : { reasoning },
|
|
184
|
+
...descriptionZh === void 0 ? {} : { descriptionZh },
|
|
185
|
+
...descriptionEn === void 0 ? {} : { descriptionEn },
|
|
186
|
+
...supportsToolCall === void 0 ? {} : { supportsToolCall }
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
var WorkBuddyUpstreamClient = class {
|
|
190
|
+
fetchImpl;
|
|
191
|
+
clientVersion;
|
|
192
|
+
constructor(options = {}) {
|
|
193
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
194
|
+
this.clientVersion = options.clientVersion ?? "2.0.4";
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
|
|
198
|
+
* force `stream: true` (the upstream rejects non-streaming), convert the
|
|
199
|
+
* DSH `developer` role into `system` (upstream rejects `developer` with
|
|
200
|
+
* business code 11128), and flatten `tool_choice` into its string form.
|
|
201
|
+
*/
|
|
202
|
+
prepareChatBody(raw) {
|
|
203
|
+
let body;
|
|
204
|
+
try {
|
|
205
|
+
body = JSON.parse(raw);
|
|
206
|
+
} catch {
|
|
207
|
+
return raw;
|
|
208
|
+
}
|
|
209
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return raw;
|
|
210
|
+
const obj = body;
|
|
211
|
+
obj["stream"] = true;
|
|
212
|
+
delete obj["stream_options"];
|
|
213
|
+
if (Array.isArray(obj["messages"])) for (const value of obj["messages"]) {
|
|
214
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
215
|
+
const message = value;
|
|
216
|
+
if (message["role"] === "developer") message["role"] = "system";
|
|
217
|
+
}
|
|
218
|
+
const choice = obj["tool_choice"];
|
|
219
|
+
if (typeof choice === "string") {
|
|
220
|
+
if (choice.trim().toLowerCase() === "none") {
|
|
221
|
+
delete obj["tool_choice"];
|
|
222
|
+
delete obj["tools"];
|
|
223
|
+
delete obj["functions"];
|
|
224
|
+
}
|
|
225
|
+
} else if (typeof choice === "object" && choice !== null && !Array.isArray(choice)) {
|
|
226
|
+
const wrapped = choice;
|
|
227
|
+
const type = typeof wrapped["type"] === "string" ? wrapped["type"].trim().toLowerCase() : "";
|
|
228
|
+
if (type === "none") {
|
|
229
|
+
delete obj["tool_choice"];
|
|
230
|
+
delete obj["tools"];
|
|
231
|
+
delete obj["functions"];
|
|
232
|
+
} else if (type === "auto" || type === "required") obj["tool_choice"] = type;
|
|
233
|
+
else if (type === "function") {
|
|
234
|
+
const fn = typeof wrapped["function"] === "object" && wrapped["function"] !== null ? wrapped["function"] : void 0;
|
|
235
|
+
let name = typeof fn?.["name"] === "string" ? fn["name"] : "";
|
|
236
|
+
if (name === "" && typeof wrapped["name"] === "string") name = wrapped["name"];
|
|
237
|
+
obj["tool_choice"] = name.trim() !== "" ? name.trim() : "auto";
|
|
238
|
+
} else delete obj["tool_choice"];
|
|
239
|
+
}
|
|
240
|
+
return JSON.stringify(obj);
|
|
241
|
+
}
|
|
242
|
+
/** Forward one chat completion. Never throws for upstream failures. */
|
|
243
|
+
async chatStream(credential, prepared, signal) {
|
|
244
|
+
let response;
|
|
245
|
+
try {
|
|
246
|
+
response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: chatHeaders(credential),
|
|
249
|
+
body: prepared,
|
|
250
|
+
...signal === void 0 ? {} : { signal }
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
kind: "server",
|
|
256
|
+
status: 0,
|
|
257
|
+
message: `transport error: ${String(error)}`
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (response.ok) return {
|
|
261
|
+
ok: true,
|
|
262
|
+
response
|
|
263
|
+
};
|
|
264
|
+
const text = (await response.text().catch(() => "")).slice(0, ERROR_BODY_LIMIT);
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
kind: classifyUpstreamError(response.status, text),
|
|
268
|
+
status: response.status,
|
|
269
|
+
message: text
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/** POST the token-refresh endpoint; the caller merges the outcome. */
|
|
273
|
+
async refreshToken(credential) {
|
|
274
|
+
const response = await this.fetchImpl(`${chatBase(credential)}/v2/plugin/auth/token/refresh`, {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: refreshHeaders(credential),
|
|
277
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
278
|
+
});
|
|
279
|
+
const envelope = await readEnvelope(response);
|
|
280
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
281
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
282
|
+
const accessToken = typeof data["accessToken"] === "string" ? data["accessToken"] : "";
|
|
283
|
+
if (accessToken === "") throw new Error("workbuddy token refresh returned no accessToken; sign in again in the WorkBuddy app");
|
|
284
|
+
const outcome = { accessToken };
|
|
285
|
+
if (typeof data["refreshToken"] === "string" && data["refreshToken"] !== "") outcome.refreshToken = data["refreshToken"];
|
|
286
|
+
if (typeof data["expiresIn"] === "number" && data["expiresIn"] > 0) outcome.expiresInSec = data["expiresIn"];
|
|
287
|
+
if (typeof data["domain"] === "string" && data["domain"] !== "") outcome.domain = data["domain"];
|
|
288
|
+
return outcome;
|
|
289
|
+
}
|
|
290
|
+
/** GET the personal model catalog, keeping the `cli` agent's models only. */
|
|
291
|
+
async fetchModels(credential, signal) {
|
|
292
|
+
const response = await this.fetchImpl(`${chatBase(credential)}/console/enterprises/personal/models`, {
|
|
293
|
+
headers: {
|
|
294
|
+
"Authorization": `Bearer ${credential.accessToken}`,
|
|
295
|
+
"Accept": "application/json",
|
|
296
|
+
"Origin": originReferer(credential),
|
|
297
|
+
"Referer": `${originReferer(credential)}/`,
|
|
298
|
+
"User-Agent": CLIENT_UA
|
|
299
|
+
},
|
|
300
|
+
...signal === void 0 ? {} : { signal }
|
|
301
|
+
});
|
|
302
|
+
const envelope = await readEnvelope(response);
|
|
303
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
304
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
305
|
+
const rawModels = Array.isArray(data["models"]) ? data["models"] : [];
|
|
306
|
+
const agents = Array.isArray(data["agents"]) ? data["agents"] : [];
|
|
307
|
+
let cliIds;
|
|
308
|
+
for (const agent of agents) if (typeof agent === "object" && agent !== null) {
|
|
309
|
+
const wrapped = agent;
|
|
310
|
+
if (wrapped["name"] === "cli" && Array.isArray(wrapped["models"])) {
|
|
311
|
+
cliIds = wrapped["models"].filter((id) => typeof id === "string");
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const byId = /* @__PURE__ */ new Map();
|
|
316
|
+
for (const model of rawModels) {
|
|
317
|
+
const parsed = parseUpstreamModel(model);
|
|
318
|
+
if (parsed !== void 0) byId.set(parsed.id, parsed);
|
|
319
|
+
}
|
|
320
|
+
const models = (cliIds !== void 0 && cliIds.length > 0 ? cliIds : [...byId.keys()]).map((id) => byId.get(id)).filter((model) => model !== void 0);
|
|
321
|
+
if (models.length === 0) throw new Error("workbuddy model catalog resolved to an empty list");
|
|
322
|
+
return models;
|
|
323
|
+
}
|
|
324
|
+
/** Read-only credits query, aggregated by package. Does not consume credits. */
|
|
325
|
+
async fetchCredits(credential) {
|
|
326
|
+
const now = /* @__PURE__ */ new Date();
|
|
327
|
+
const fmt = (date) => {
|
|
328
|
+
const p = (n) => n.toString().padStart(2, "0");
|
|
329
|
+
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
|
|
330
|
+
};
|
|
331
|
+
const response = await this.fetchImpl(`${billingBase(credential)}/v2/billing/meter/get-user-resource`, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: billingHeaders(credential),
|
|
334
|
+
body: JSON.stringify({
|
|
335
|
+
PageNumber: 1,
|
|
336
|
+
PageSize: 100,
|
|
337
|
+
ProductCode: "p_tcaca",
|
|
338
|
+
Status: [0, 3],
|
|
339
|
+
PackageEndTimeRangeBegin: fmt(now),
|
|
340
|
+
PackageEndTimeRangeEnd: fmt(new Date(now.getTime() + 3185136e6))
|
|
341
|
+
}),
|
|
342
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
343
|
+
});
|
|
344
|
+
const envelope = await readEnvelope(response);
|
|
345
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
346
|
+
const wrapper = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
347
|
+
const data = typeof wrapper["Response"] === "object" && wrapper["Response"] !== null ? wrapper["Response"] : {};
|
|
348
|
+
const inner = typeof data["Data"] === "object" && data["Data"] !== null ? data["Data"] : {};
|
|
349
|
+
const rawAccounts = Array.isArray(inner["Accounts"]) ? inner["Accounts"] : [];
|
|
350
|
+
let total = 0;
|
|
351
|
+
let nearestExpiryMs;
|
|
352
|
+
let expiringSoon = 0;
|
|
353
|
+
const SOON_MS = 2592e5;
|
|
354
|
+
const parseDate = (raw) => {
|
|
355
|
+
if (typeof raw === "number" && raw > 0xe8d4a51000) return raw;
|
|
356
|
+
if (typeof raw === "string" && raw !== "") {
|
|
357
|
+
const parsed = Date.parse(raw);
|
|
358
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const packages = [];
|
|
362
|
+
for (const raw of rawAccounts) {
|
|
363
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
364
|
+
const account = raw;
|
|
365
|
+
const num = (key) => typeof account[key] === "number" ? account[key] : 0;
|
|
366
|
+
const monthly = num("CapacityType") === 4;
|
|
367
|
+
const size = monthly ? num("CycleCapacitySize") : num("CapacitySize");
|
|
368
|
+
const remain = monthly ? num("CycleCapacityRemain") : num("CapacityRemain");
|
|
369
|
+
const capped = remain < 0 ? 0 : remain;
|
|
370
|
+
const cycleEndMs = parseDate(account["CycleEndTime"]);
|
|
371
|
+
const expiresAtMs = monthly ? void 0 : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
|
|
372
|
+
const refreshAtMs = monthly ? cycleEndMs === void 0 ? void 0 : cycleEndMs + 1e3 : void 0;
|
|
373
|
+
if (!monthly && (capped <= 0 || expiresAtMs !== void 0 && expiresAtMs <= Date.now())) continue;
|
|
374
|
+
total += capped;
|
|
375
|
+
if (expiresAtMs !== void 0) {
|
|
376
|
+
if (nearestExpiryMs === void 0 || expiresAtMs < nearestExpiryMs) nearestExpiryMs = expiresAtMs;
|
|
377
|
+
if (expiresAtMs - Date.now() <= SOON_MS) expiringSoon += capped;
|
|
378
|
+
}
|
|
379
|
+
packages.push({
|
|
380
|
+
packageName: typeof account["PackageName"] === "string" ? account["PackageName"] : "(unnamed)",
|
|
381
|
+
remain: capped,
|
|
382
|
+
size,
|
|
383
|
+
monthly,
|
|
384
|
+
...refreshAtMs === void 0 ? {} : { refreshAtMs },
|
|
385
|
+
...expiresAtMs === void 0 ? {} : { expiresAtMs }
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
total,
|
|
390
|
+
packages,
|
|
391
|
+
expiringSoon,
|
|
392
|
+
...nearestExpiryMs === void 0 ? {} : { nearestExpiryMs }
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/** Query today's check-in status without changing account state. */
|
|
396
|
+
async fetchCheckinStatus(credential) {
|
|
397
|
+
const response = await this.fetchImpl(`${billingBase(credential)}/v2/billing/meter/checkin-activity-status`, {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: billingHeaders(credential),
|
|
400
|
+
body: "{}",
|
|
401
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
402
|
+
});
|
|
403
|
+
const envelope = await readEnvelope(response);
|
|
404
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
405
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
406
|
+
const num = (key) => typeof data[key] === "number" ? data[key] : 0;
|
|
407
|
+
return {
|
|
408
|
+
active: data["active"] === true,
|
|
409
|
+
todayCheckedIn: data["today_checked_in"] === true,
|
|
410
|
+
streakDays: num("streak_days"),
|
|
411
|
+
dailyCredit: num("daily_credit"),
|
|
412
|
+
todayCredit: num("today_credit"),
|
|
413
|
+
isStreakDay: data["is_streak_day"] === true,
|
|
414
|
+
nextStreakDay: num("next_streak_day"),
|
|
415
|
+
streakBonusDays: num("streak_bonus_days"),
|
|
416
|
+
streakBonusCredit: num("streak_bonus_credit"),
|
|
417
|
+
...typeof data["claim_button_text"] === "string" && data["claim_button_text"] !== "" ? { claimButtonText: data["claim_button_text"] } : {}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/** Claim today's check-in reward. The browser route guards this mutation. */
|
|
421
|
+
async claimDailyCheckin(credential) {
|
|
422
|
+
const response = await this.fetchImpl(`${billingBase(credential)}/v2/billing/meter/daily-checkin`, {
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: billingHeaders(credential),
|
|
425
|
+
body: "{}",
|
|
426
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
427
|
+
});
|
|
428
|
+
const envelope = await readEnvelope(response);
|
|
429
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
430
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
431
|
+
const numberField = (key) => typeof data[key] === "number" ? data[key] : 0;
|
|
432
|
+
return {
|
|
433
|
+
credit: numberField("credit"),
|
|
434
|
+
streakDays: numberField("streak_days"),
|
|
435
|
+
isStreakDay: data["is_streak_day"] === true
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** Legacy thin wrapper kept for `status`/`doctor`: returns raw envelope data. */
|
|
439
|
+
async credits(credential) {
|
|
440
|
+
try {
|
|
441
|
+
return {
|
|
442
|
+
ok: true,
|
|
443
|
+
data: await this.fetchCredits(credential)
|
|
444
|
+
};
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return {
|
|
447
|
+
ok: false,
|
|
448
|
+
message: String(error)
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/accounts.ts
|
|
455
|
+
/**
|
|
456
|
+
* Account pool: discovers every WorkBuddy credential snapshot the desktop app
|
|
457
|
+
* has left on this machine and hands out one healthy account per request,
|
|
458
|
+
* rotating away from any account the upstream has rate-limited.
|
|
459
|
+
*
|
|
460
|
+
* Discovery is read-only: the desktop app's files are never written. Each
|
|
461
|
+
* account is keyed by its billing identity (`uin`, falling back to `uid`), so
|
|
462
|
+
* re-logging the same account refreshes in place instead of creating a duplicate.
|
|
463
|
+
*
|
|
464
|
+
* @module dsh-workbuddy-xdpool/accounts
|
|
465
|
+
*/
|
|
466
|
+
/** Live auth file name the WorkBuddy desktop app writes. */
|
|
467
|
+
const WORKBUDDY_LIVE_FILENAME = "workbuddy-desktop.info";
|
|
468
|
+
/** Snapshot files left behind by previous logins share this prefix. */
|
|
469
|
+
const SNAPSHOT_PREFIX = "workbuddy-desktop.";
|
|
470
|
+
/** Env override for the auth file or its directory. */
|
|
471
|
+
const WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
|
|
472
|
+
function nonEmptyEnv(value) {
|
|
473
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Platform-default directories holding the desktop app's auth files.
|
|
477
|
+
* Windows probes Local before Roaming; a redirected profile still resolves
|
|
478
|
+
* through the env location.
|
|
479
|
+
*/
|
|
480
|
+
function defaultDesktopAuthDirs(platform = process.platform, home = homedir(), env = process.env) {
|
|
481
|
+
if (platform === "darwin") return [join(home, "Library", "Application Support", "CodeBuddyExtension", "Data", "Public", "auth")];
|
|
482
|
+
if (platform === "win32") {
|
|
483
|
+
const local = nonEmptyEnv(env["LOCALAPPDATA"]) ?? join(home, "AppData", "Local");
|
|
484
|
+
const roaming = nonEmptyEnv(env["APPDATA"]) ?? join(home, "AppData", "Roaming");
|
|
485
|
+
return [join(local, "CodeBuddyExtension", "Data", "Public", "auth"), join(roaming, "CodeBuddyExtension", "Data", "Public", "auth")];
|
|
486
|
+
}
|
|
487
|
+
if (platform === "linux") {
|
|
488
|
+
const config = nonEmptyEnv(env["XDG_CONFIG_HOME"]) ?? join(home, ".config");
|
|
489
|
+
return [join(config, "CodeBuddyExtension", "Data", "Public", "auth")];
|
|
490
|
+
}
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
/** Normalize an expiry that may arrive in seconds or milliseconds. */
|
|
494
|
+
function expiryToMs(value) {
|
|
495
|
+
if (value <= 0) return 0;
|
|
496
|
+
return value > 0xe8d4a51000 ? value : value * 1e3;
|
|
497
|
+
}
|
|
498
|
+
function optionalString(value) {
|
|
499
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Parse a WorkBuddy auth document. Accepts the nested desktop shape
|
|
503
|
+
* `{"auth":{...},"account":{...}}` and the flat panel shape; returns undefined
|
|
504
|
+
* when there is no usable access token.
|
|
505
|
+
*/
|
|
506
|
+
function parseWorkBuddyAuth(text, sourcePath) {
|
|
507
|
+
let parsed;
|
|
508
|
+
try {
|
|
509
|
+
parsed = JSON.parse(text);
|
|
510
|
+
} catch {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
514
|
+
const document = parsed;
|
|
515
|
+
let auth;
|
|
516
|
+
let identity;
|
|
517
|
+
if (typeof document["auth"] === "object" && document["auth"] !== null) {
|
|
518
|
+
auth = document["auth"];
|
|
519
|
+
identity = typeof document["account"] === "object" && document["account"] !== null ? document["account"] : {};
|
|
520
|
+
} else {
|
|
521
|
+
auth = document;
|
|
522
|
+
identity = document;
|
|
523
|
+
}
|
|
524
|
+
const accessToken = typeof auth["accessToken"] === "string" ? auth["accessToken"] : "";
|
|
525
|
+
if (accessToken === "") return void 0;
|
|
526
|
+
const refreshExpiresAtMs = typeof auth["refreshExpiresAt"] === "number" ? expiryToMs(auth["refreshExpiresAt"]) : void 0;
|
|
527
|
+
if (refreshExpiresAtMs !== void 0 && refreshExpiresAtMs > 0 && refreshExpiresAtMs < Date.now()) return;
|
|
528
|
+
return {
|
|
529
|
+
accessToken,
|
|
530
|
+
refreshToken: typeof auth["refreshToken"] === "string" ? auth["refreshToken"] : "",
|
|
531
|
+
expiresAtMs: typeof auth["expiresAt"] === "number" ? expiryToMs(auth["expiresAt"]) : 0,
|
|
532
|
+
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
533
|
+
...optionalString(identity["nickname"]) === void 0 ? {} : { nickname: optionalString(identity["nickname"]) },
|
|
534
|
+
...optionalString(identity["uin"]) === void 0 ? {} : { uin: optionalString(identity["uin"]) },
|
|
535
|
+
...optionalString(identity["uid"]) === void 0 ? {} : { uid: optionalString(identity["uid"]) },
|
|
536
|
+
...optionalString(identity["enterpriseId"]) === void 0 ? {} : { enterpriseId: optionalString(identity["enterpriseId"]) },
|
|
537
|
+
domain: typeof auth["domain"] === "string" ? auth["domain"] : "",
|
|
538
|
+
sourcePath
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Stable account id. `uin` is the billing identity the upstream keys on and
|
|
543
|
+
* survives re-login; `uid` is the fallback.
|
|
544
|
+
*/
|
|
545
|
+
function workbuddyAccountId(credential) {
|
|
546
|
+
const stable = credential.uin ?? credential.uid ?? credential.nickname ?? "unknown";
|
|
547
|
+
return createHash("sha256").update(`workbuddy\0${stable}`).digest("hex").slice(0, 16);
|
|
548
|
+
}
|
|
549
|
+
/** Human label; distinguishes same-nickname accounts by uid prefix. */
|
|
550
|
+
function accountLabel(credential) {
|
|
551
|
+
const name = credential.nickname ?? "WorkBuddy";
|
|
552
|
+
const discriminator = (credential.uid ?? credential.uin ?? "").slice(0, 8);
|
|
553
|
+
return discriminator === "" ? name : `${name}#${discriminator}`;
|
|
554
|
+
}
|
|
555
|
+
/** List the auth files in one directory: the live file plus every snapshot. */
|
|
556
|
+
async function authFilesIn(dir) {
|
|
557
|
+
let entries;
|
|
558
|
+
try {
|
|
559
|
+
entries = await readdir(dir);
|
|
560
|
+
} catch {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
const files = entries.filter((name) => {
|
|
564
|
+
if (name === "workbuddy-desktop.info") return true;
|
|
565
|
+
return name.startsWith(SNAPSHOT_PREFIX) && name.endsWith(".info");
|
|
566
|
+
});
|
|
567
|
+
files.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
|
|
568
|
+
return files.map((name) => join(dir, name));
|
|
569
|
+
}
|
|
570
|
+
async function readCredential(path) {
|
|
571
|
+
try {
|
|
572
|
+
return parseWorkBuddyAuth(await readFile(path, "utf8"), path);
|
|
573
|
+
} catch {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/** Every directory the pool should scan, in probe order. */
|
|
578
|
+
function candidateAuthDirs(env = process.env) {
|
|
579
|
+
const dirs = [];
|
|
580
|
+
const override = nonEmptyEnv(env[WORKBUDDY_AUTH_FILE_ENV]);
|
|
581
|
+
if (override !== void 0) dirs.push(override.toLowerCase().endsWith(".info") ? resolve(override, "..") : override);
|
|
582
|
+
dirs.push(...defaultDesktopAuthDirs(process.env["DSH_TEST_PLATFORM"]));
|
|
583
|
+
return dirs;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Read-only pool of every discovered WorkBuddy account, with rate-limit
|
|
587
|
+
* cooldown and round-robin failover.
|
|
588
|
+
*/
|
|
589
|
+
var WorkBuddyAccountPool = class {
|
|
590
|
+
logger;
|
|
591
|
+
authDirs;
|
|
592
|
+
cooldownMs;
|
|
593
|
+
client;
|
|
594
|
+
refreshMarginMs;
|
|
595
|
+
accounts = [];
|
|
596
|
+
cursor = 0;
|
|
597
|
+
lastScanAtMs = 0;
|
|
598
|
+
preferredId;
|
|
599
|
+
refreshInflight = /* @__PURE__ */ new Map();
|
|
600
|
+
constructor(options = {}) {
|
|
601
|
+
this.logger = options.logger;
|
|
602
|
+
this.authDirs = options.authDirs ?? candidateAuthDirs();
|
|
603
|
+
this.cooldownMs = options.cooldownMs ?? 6e4;
|
|
604
|
+
this.client = options.client;
|
|
605
|
+
this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Re-apply configuration that only affects discovery and cooldown policy,
|
|
609
|
+
* without rebuilding the pool. A later `scan()` uses the new auth dirs and
|
|
610
|
+
* cooldown window; existing accounts keep their in-memory state.
|
|
611
|
+
*/
|
|
612
|
+
applyConfig(options) {
|
|
613
|
+
if (options.authDirs !== void 0 && options.authDirs.length > 0) this.authDirs = options.authDirs;
|
|
614
|
+
if (options.cooldownMs !== void 0 && options.cooldownMs >= 1e3) this.cooldownMs = options.cooldownMs;
|
|
615
|
+
}
|
|
616
|
+
/** Rescan the auth directories and merge newly discovered accounts. */
|
|
617
|
+
async scan() {
|
|
618
|
+
const found = [];
|
|
619
|
+
for (const dir of this.authDirs) for (const file of await authFilesIn(dir)) {
|
|
620
|
+
const credential = await readCredential(file);
|
|
621
|
+
if (credential !== void 0) found.push(credential);
|
|
622
|
+
}
|
|
623
|
+
const byId = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const account of this.accounts) byId.set(account.id, account);
|
|
625
|
+
for (const credential of found) {
|
|
626
|
+
const id = workbuddyAccountId(credential);
|
|
627
|
+
const existing = byId.get(id);
|
|
628
|
+
if (existing === void 0) {
|
|
629
|
+
byId.set(id, {
|
|
630
|
+
id,
|
|
631
|
+
label: accountLabel(credential),
|
|
632
|
+
credential,
|
|
633
|
+
cooldownUntilMs: 0,
|
|
634
|
+
modelCooldowns: {},
|
|
635
|
+
rateLimitHits: 0
|
|
636
|
+
});
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if ((credential.expiresAtMs ?? 0) > (existing.credential.expiresAtMs ?? 0)) byId.set(id, {
|
|
640
|
+
...existing,
|
|
641
|
+
credential,
|
|
642
|
+
label: accountLabel(credential)
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
this.accounts = [...byId.values()];
|
|
646
|
+
this.lastScanAtMs = Date.now();
|
|
647
|
+
return this.accounts;
|
|
648
|
+
}
|
|
649
|
+
/** All accounts, cooldown state included. */
|
|
650
|
+
list(region) {
|
|
651
|
+
if (region === void 0) return this.accounts;
|
|
652
|
+
return this.accounts.filter((account) => regionOf(account.credential.domain) === region);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Accounts currently eligible to serve a request.
|
|
656
|
+
*
|
|
657
|
+
* With a `modelId`, an account is eligible when it is not account-wide cooled
|
|
658
|
+
* AND that model is not cooling on it — so a 429 on `hy4-preview` only keeps
|
|
659
|
+
* that model out while `hy3` on the same account stays usable. Without a
|
|
660
|
+
* model id the legacy account-wide check applies (callers that cannot name a
|
|
661
|
+
* model, e.g. CLI diagnostics).
|
|
662
|
+
*/
|
|
663
|
+
available(now, modelId, region) {
|
|
664
|
+
return this.accounts.filter((account) => {
|
|
665
|
+
if (account.cooldownUntilMs > now) return false;
|
|
666
|
+
if (modelId !== void 0 && (account.modelCooldowns[modelId] ?? 0) > now) return false;
|
|
667
|
+
if (region !== void 0 && regionOf(account.credential.domain) !== region) return false;
|
|
668
|
+
return true;
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Pick the next usable account for an optional model. Scans on first use,
|
|
673
|
+
* and rescans when every known account is cooling down — a fresh desktop
|
|
674
|
+
* login is the usual way out of an exhausted pool. A preferred
|
|
675
|
+
* (user-selected) account that is healthy is tried first; otherwise the
|
|
676
|
+
* cursor round-robins so consecutive requests spread across accounts and a
|
|
677
|
+
* still-cooling preferred account is skipped.
|
|
678
|
+
*/
|
|
679
|
+
async acquire(modelId, region) {
|
|
680
|
+
if (this.accounts.length === 0) await this.scan();
|
|
681
|
+
let pool = this.available(Date.now(), modelId, region);
|
|
682
|
+
if (pool.length === 0) {
|
|
683
|
+
await this.scan();
|
|
684
|
+
pool = this.available(Date.now(), modelId, region);
|
|
685
|
+
}
|
|
686
|
+
if (pool.length === 0) return void 0;
|
|
687
|
+
let start = this.cursor % pool.length;
|
|
688
|
+
if (this.preferredId !== void 0) {
|
|
689
|
+
const preferredIndex = pool.findIndex((account) => account.id === this.preferredId);
|
|
690
|
+
if (preferredIndex !== -1) start = preferredIndex;
|
|
691
|
+
}
|
|
692
|
+
for (let step = 0; step < pool.length; step += 1) {
|
|
693
|
+
const account = pool[(start + step) % pool.length];
|
|
694
|
+
if (account === void 0) continue;
|
|
695
|
+
await this.ensureFresh(account);
|
|
696
|
+
this.cursor = (start + step + 1) % pool.length;
|
|
697
|
+
return account;
|
|
698
|
+
}
|
|
699
|
+
return pool[0];
|
|
700
|
+
}
|
|
701
|
+
/** Pin the account the plugin card should prefer; tokens stay out of settings. */
|
|
702
|
+
prefer(accountId) {
|
|
703
|
+
this.preferredId = accountId;
|
|
704
|
+
}
|
|
705
|
+
/** Best-effort refresh of one account after a session-dead upstream answer. */
|
|
706
|
+
async refreshAccount(accountId) {
|
|
707
|
+
const account = this.accounts.find((item) => item.id === accountId);
|
|
708
|
+
if (account === void 0) return;
|
|
709
|
+
await this.ensureFresh(account);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Refresh the account's access token when it is within the margin (or already
|
|
713
|
+
* expired), in-flight de-duped per account. A failed refresh keeps the
|
|
714
|
+
* existing token when it has not yet expired, so an unreachable refresh
|
|
715
|
+
* endpoint never takes down a working session.
|
|
716
|
+
*/
|
|
717
|
+
async ensureFresh(account) {
|
|
718
|
+
if (this.client === void 0) return;
|
|
719
|
+
const credential = account.credential;
|
|
720
|
+
if (!(credential.expiresAtMs <= 0 || credential.expiresAtMs <= Date.now() + this.refreshMarginMs)) return;
|
|
721
|
+
const existing = this.refreshInflight.get(account.id);
|
|
722
|
+
if (existing !== void 0) {
|
|
723
|
+
await existing;
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const run = (async () => {
|
|
727
|
+
if (credential.refreshToken === "") {
|
|
728
|
+
if (credential.expiresAtMs > Date.now() + 3e4) return;
|
|
729
|
+
this.logger?.warn(`dsh-workbuddy-xdpool: ${account.label} token expired with no refresh token; sign in again`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const outcome = await this.client.refreshToken(credential);
|
|
734
|
+
account.credential = {
|
|
735
|
+
...credential,
|
|
736
|
+
accessToken: outcome.accessToken,
|
|
737
|
+
...outcome.refreshToken === void 0 ? {} : { refreshToken: outcome.refreshToken },
|
|
738
|
+
expiresAtMs: outcome.expiresInSec !== void 0 ? Date.now() + outcome.expiresInSec * 1e3 : credential.expiresAtMs,
|
|
739
|
+
...outcome.domain === void 0 || outcome.domain === "" ? {} : { domain: outcome.domain }
|
|
740
|
+
};
|
|
741
|
+
this.logger?.info?.(`dsh-workbuddy-xdpool: refreshed token for ${account.label}`);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (credential.expiresAtMs > Date.now() + 3e4) this.logger?.warn?.(`dsh-workbuddy-xdpool: token refresh failed but token still valid for ${account.label}`, error);
|
|
744
|
+
else this.logger?.error?.(`dsh-workbuddy-xdpool: token refresh failed and token expired for ${account.label}`, error);
|
|
745
|
+
}
|
|
746
|
+
})();
|
|
747
|
+
this.refreshInflight.set(account.id, run);
|
|
748
|
+
try {
|
|
749
|
+
await run;
|
|
750
|
+
} finally {
|
|
751
|
+
this.refreshInflight.delete(account.id);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Mark an account (or one of its models) rate-limited.
|
|
756
|
+
*
|
|
757
|
+
* With `modelId`, only that model on the account is cooled — the account's
|
|
758
|
+
* other models stay in rotation, matching the upstream's per-model rate
|
|
759
|
+
* limit ("可切换其他模型继续使用"). Without a model id the whole account is
|
|
760
|
+
* cooled, which callers should reserve for limits that truly span every model.
|
|
761
|
+
*/
|
|
762
|
+
penalize(accountId, resetAtMs, modelId) {
|
|
763
|
+
const account = this.accounts.find((item) => item.id === accountId);
|
|
764
|
+
if (account === void 0) return;
|
|
765
|
+
account.rateLimitHits += 1;
|
|
766
|
+
const until = resetAtMs ?? Date.now() + this.cooldownMs;
|
|
767
|
+
if (modelId !== void 0 && modelId !== "") {
|
|
768
|
+
account.modelCooldowns[modelId] = Math.max(account.modelCooldowns[modelId] ?? 0, until);
|
|
769
|
+
this.logger?.warn(`dsh-workbuddy-xdpool: ${account.label} rate-limited on model ${modelId}; cooling that model until ${new Date(until).toISOString()}`);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
account.cooldownUntilMs = Math.max(account.cooldownUntilMs, until);
|
|
773
|
+
this.logger?.warn(`dsh-workbuddy-xdpool: account ${account.label} rate-limited; cooling until ${new Date(until).toISOString()}`);
|
|
774
|
+
}
|
|
775
|
+
/** Clear all cooldowns (account-wide and per-model), e.g. from a reset command. */
|
|
776
|
+
resetCooldowns() {
|
|
777
|
+
for (const account of this.accounts) {
|
|
778
|
+
account.cooldownUntilMs = 0;
|
|
779
|
+
account.modelCooldowns = {};
|
|
780
|
+
account.rateLimitHits = 0;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/** Diagnostics snapshot. Account-wide cooling count (per-model cooling excluded:
|
|
784
|
+
* the account as a whole stays usable when only one model is limited). */
|
|
785
|
+
status() {
|
|
786
|
+
const now = Date.now();
|
|
787
|
+
return {
|
|
788
|
+
count: this.accounts.length,
|
|
789
|
+
cooling: this.accounts.filter((account) => account.cooldownUntilMs > now).length,
|
|
790
|
+
lastScanAtMs: this.lastScanAtMs
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region src/catalog.ts
|
|
796
|
+
/** Static fallback used before the first live catalog fetch. */
|
|
797
|
+
const FALLBACK_WORKBUDDY_MODELS = [
|
|
798
|
+
{
|
|
799
|
+
id: "glm-5.3",
|
|
800
|
+
name: "GLM-5.3",
|
|
801
|
+
contextWindow: 2e5,
|
|
802
|
+
maxOutputTokens: 128e3,
|
|
803
|
+
supportsImages: true
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
id: "glm-5.3-flash",
|
|
807
|
+
name: "GLM-5.3-Flash",
|
|
808
|
+
contextWindow: 2e5,
|
|
809
|
+
maxOutputTokens: 128e3,
|
|
810
|
+
supportsImages: true
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
id: "glm-5.2",
|
|
814
|
+
name: "GLM-5.2",
|
|
815
|
+
contextWindow: 2e5,
|
|
816
|
+
maxOutputTokens: 128e3,
|
|
817
|
+
supportsImages: true
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
id: "glm-5.1",
|
|
821
|
+
name: "GLM-5.1",
|
|
822
|
+
contextWindow: 2e5,
|
|
823
|
+
maxOutputTokens: 128e3,
|
|
824
|
+
supportsImages: false
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
id: "deepseek-v4-pro",
|
|
828
|
+
name: "DeepSeek-V4-Pro",
|
|
829
|
+
contextWindow: 2e5,
|
|
830
|
+
maxOutputTokens: 128e3,
|
|
831
|
+
supportsImages: true
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
id: "deepseek-v4-flash",
|
|
835
|
+
name: "DeepSeek-V4-Flash",
|
|
836
|
+
contextWindow: 2e5,
|
|
837
|
+
maxOutputTokens: 128e3,
|
|
838
|
+
supportsImages: true
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
id: "kimi-k3",
|
|
842
|
+
name: "Kimi-K3",
|
|
843
|
+
contextWindow: 2e5,
|
|
844
|
+
maxOutputTokens: 128e3,
|
|
845
|
+
supportsImages: true
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
id: "minimax-m3",
|
|
849
|
+
name: "MiniMax-M3",
|
|
850
|
+
contextWindow: 2e5,
|
|
851
|
+
maxOutputTokens: 128e3,
|
|
852
|
+
supportsImages: true
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
id: "hy3",
|
|
856
|
+
name: "Hy3",
|
|
857
|
+
contextWindow: 2e5,
|
|
858
|
+
maxOutputTokens: 128e3,
|
|
859
|
+
supportsImages: true
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
id: "hy4-preview",
|
|
863
|
+
name: "Hy4-Preview",
|
|
864
|
+
contextWindow: 1e6,
|
|
865
|
+
maxOutputTokens: 128e3,
|
|
866
|
+
supportsImages: true
|
|
867
|
+
}
|
|
868
|
+
];
|
|
869
|
+
/** Live catalog with a static fallback behind it. */
|
|
870
|
+
var WorkBuddyCatalog = class {
|
|
871
|
+
models = FALLBACK_WORKBUDDY_MODELS;
|
|
872
|
+
listeners = /* @__PURE__ */ new Set();
|
|
873
|
+
/** User's model selection. Empty object = follow the catalog unfiltered. */
|
|
874
|
+
selection = {};
|
|
875
|
+
current() {
|
|
876
|
+
return this.models;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* The models DSH should actually offer, after applying the user's selection:
|
|
880
|
+
* disabled models are dropped, an explicit image list overrides the upstream
|
|
881
|
+
* capability flag, and a per-model budget caps the advertised window.
|
|
882
|
+
*
|
|
883
|
+
* An absent `enabledModelIds` means "everything" — a fresh install with no
|
|
884
|
+
* saved selection must not present an empty picker.
|
|
885
|
+
*/
|
|
886
|
+
visible() {
|
|
887
|
+
const enabled = this.selection.enabledModelIds;
|
|
888
|
+
const allow = enabled === void 0 ? void 0 : new Set(enabled);
|
|
889
|
+
const images = this.selection.imageModelIds;
|
|
890
|
+
const imageSet = images === void 0 ? void 0 : new Set(images);
|
|
891
|
+
const budgets = this.selection.contextBudgets;
|
|
892
|
+
return this.models.filter((model) => allow === void 0 || allow.has(model.id)).map((model) => {
|
|
893
|
+
const next = { ...model };
|
|
894
|
+
if (imageSet !== void 0) next.supportsImages = imageSet.has(model.id);
|
|
895
|
+
const budget = budgets?.[model.id];
|
|
896
|
+
if (budget !== void 0 && budget > 0 && budget < next.contextWindow) next.contextWindow = budget;
|
|
897
|
+
return next;
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
/** Replace the catalog and notify the adapter to rebuild its model list. */
|
|
901
|
+
update(models) {
|
|
902
|
+
if (models.length === 0) return;
|
|
903
|
+
this.models = models;
|
|
904
|
+
this.notify();
|
|
905
|
+
}
|
|
906
|
+
/** Restore the static fallback, e.g. when the upstream stops answering. */
|
|
907
|
+
reset() {
|
|
908
|
+
this.models = FALLBACK_WORKBUDDY_MODELS;
|
|
909
|
+
this.notify();
|
|
910
|
+
}
|
|
911
|
+
/** Replace the user's selection; the adapter rebuilds from `visible()`. */
|
|
912
|
+
applySelection(selection) {
|
|
913
|
+
this.selection = selection;
|
|
914
|
+
this.notify();
|
|
915
|
+
}
|
|
916
|
+
/** The selection currently in force, for the card's save round-trip. */
|
|
917
|
+
currentSelection() {
|
|
918
|
+
return this.selection;
|
|
919
|
+
}
|
|
920
|
+
onChange(listener) {
|
|
921
|
+
this.listeners.add(listener);
|
|
922
|
+
return () => this.listeners.delete(listener);
|
|
923
|
+
}
|
|
924
|
+
find(id) {
|
|
925
|
+
return this.models.find((model) => model.id === id);
|
|
926
|
+
}
|
|
927
|
+
/** Replace the catalog from the live upstream list; keeps the fallback if empty. */
|
|
928
|
+
updateFromUpstream(models) {
|
|
929
|
+
this.update(catalogFromUpstream(models));
|
|
930
|
+
}
|
|
931
|
+
notify() {
|
|
932
|
+
for (const listener of this.listeners) listener();
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
/** Convert one upstream catalog entry into the plugin's model-info shape. */
|
|
936
|
+
function toModelInfo(model) {
|
|
937
|
+
return {
|
|
938
|
+
id: model.id,
|
|
939
|
+
name: model.name,
|
|
940
|
+
contextWindow: model.contextWindow,
|
|
941
|
+
maxOutputTokens: model.maxTokens,
|
|
942
|
+
supportsImages: model.multimodal ?? false,
|
|
943
|
+
...model.creditMultiplier === void 0 ? {} : { multiplier: model.creditMultiplier },
|
|
944
|
+
...model.reasoning?.supportedEfforts === void 0 ? {} : { supportedEfforts: model.reasoning.supportedEfforts }
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/** Map the live upstream list, falling back to the static list when empty. */
|
|
948
|
+
function catalogFromUpstream(models) {
|
|
949
|
+
if (models.length === 0) return FALLBACK_WORKBUDDY_MODELS;
|
|
950
|
+
return models.map(toModelInfo);
|
|
951
|
+
}
|
|
952
|
+
//#endregion
|
|
953
|
+
//#region src/status.ts
|
|
954
|
+
/** Format the per-model credit multipliers. */
|
|
955
|
+
function formatRates(status) {
|
|
956
|
+
const lines = ["Model credit multipliers:"];
|
|
957
|
+
for (const model of status.models) {
|
|
958
|
+
const rate = model.multiplier === void 0 ? "x?" : `x${model.multiplier.toFixed(2)}`;
|
|
959
|
+
lines.push(` ${model.name.padEnd(20)} ${rate}`);
|
|
960
|
+
}
|
|
961
|
+
return lines.join("\n");
|
|
962
|
+
}
|
|
963
|
+
z.object({
|
|
964
|
+
authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app own location)"),
|
|
965
|
+
cooldownMs: z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds"),
|
|
966
|
+
enabledModelIds: z.array(z.string()).default([]).description("Model ids enabled in the picker (empty = all)"),
|
|
967
|
+
imageModelIds: z.array(z.string()).default([]).description("Model ids accepting image input (empty = follow upstream)"),
|
|
968
|
+
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Per-model context-window override, keyed by model id")
|
|
969
|
+
});
|
|
970
|
+
/** Assemble the runtime objects without registering anything. */
|
|
971
|
+
function createCore(logger) {
|
|
972
|
+
const client = new WorkBuddyUpstreamClient();
|
|
973
|
+
return {
|
|
974
|
+
pool: new WorkBuddyAccountPool({
|
|
975
|
+
...logger === void 0 ? {} : { logger },
|
|
976
|
+
client
|
|
977
|
+
}),
|
|
978
|
+
catalog: new WorkBuddyCatalog(),
|
|
979
|
+
client
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/bin.ts
|
|
984
|
+
/**
|
|
985
|
+
* Command-line diagnostics and account management.
|
|
986
|
+
*
|
|
987
|
+
* dsh plugin --profile desktop exec dsh-workbuddy-xdpool <command>
|
|
988
|
+
*
|
|
989
|
+
* @module dsh-workbuddy-xdpool/bin
|
|
990
|
+
*/
|
|
991
|
+
/** Directory holding imported account snapshots. */
|
|
992
|
+
const ACCOUNT_DIR_NAME = ".workbuddy-xdpool";
|
|
993
|
+
/** Snapshot files are named by the md5 prefix of their key, so any key is safe. */
|
|
994
|
+
function snapshotPath(key, dir) {
|
|
995
|
+
return join(dir, `${createHash("md5").update(key).digest("hex").slice(0, 8)}.json`);
|
|
996
|
+
}
|
|
997
|
+
function dshHome() {
|
|
998
|
+
const fromEnv = process.env["DSH_HOME"];
|
|
999
|
+
if (typeof fromEnv === "string" && fromEnv.trim() !== "") return fromEnv.trim();
|
|
1000
|
+
return join(homedir(), ".dsh");
|
|
1001
|
+
}
|
|
1002
|
+
function accountDir() {
|
|
1003
|
+
return join(dshHome(), ACCOUNT_DIR_NAME);
|
|
1004
|
+
}
|
|
1005
|
+
function usage() {
|
|
1006
|
+
return [
|
|
1007
|
+
"dsh-workbuddy-xdpool — multi-account WorkBuddy provider for DeepSeek Harness",
|
|
1008
|
+
"",
|
|
1009
|
+
"Usage: dsh-workbuddy-xdpool <command> [options]",
|
|
1010
|
+
"",
|
|
1011
|
+
"Commands:",
|
|
1012
|
+
" status Account pool, credits, and shim state (add --credits, --json, --rates)",
|
|
1013
|
+
" doctor Diagnose discovery, cooldowns, and upstream reachability",
|
|
1014
|
+
" accounts List discovered accounts (add --json)",
|
|
1015
|
+
" import <key> Snapshot the current desktop login as <key> (add --force)",
|
|
1016
|
+
" remove <key> Delete one imported snapshot",
|
|
1017
|
+
" login Guide for adding another account (desktop app is single-sign-in)",
|
|
1018
|
+
" checkin [all|<acct>] Daily check-in: report status, or collect with `all` / a label",
|
|
1019
|
+
" reset Clear all rate-limit cooldowns immediately",
|
|
1020
|
+
"",
|
|
1021
|
+
"Options:",
|
|
1022
|
+
" --json Machine-readable output",
|
|
1023
|
+
" --credits Query remaining credits (read-only; does not consume)",
|
|
1024
|
+
" --rates Show per-model credit multipliers",
|
|
1025
|
+
" --force Overwrite an existing snapshot"
|
|
1026
|
+
].join("\n");
|
|
1027
|
+
}
|
|
1028
|
+
/** Live auth files, in probe order. */
|
|
1029
|
+
function liveCandidates() {
|
|
1030
|
+
const fromEnv = process.env["WORKBUDDY_AUTH_FILE"];
|
|
1031
|
+
if (typeof fromEnv === "string" && fromEnv.trim() !== "") return [resolve(fromEnv.trim())];
|
|
1032
|
+
return defaultDesktopAuthDirs().map((dir) => join(dir, WORKBUDDY_LIVE_FILENAME));
|
|
1033
|
+
}
|
|
1034
|
+
async function readJsonFile(path) {
|
|
1035
|
+
try {
|
|
1036
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
1037
|
+
} catch {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
async function commandStatus(args) {
|
|
1042
|
+
const asJson = args.includes("--json");
|
|
1043
|
+
const withCredits = args.includes("--credits");
|
|
1044
|
+
const withRates = args.includes("--rates");
|
|
1045
|
+
const core = createCore();
|
|
1046
|
+
const accounts = await core.pool.scan();
|
|
1047
|
+
const status = {
|
|
1048
|
+
ok: accounts.length > 0,
|
|
1049
|
+
accounts: accounts.map((account) => {
|
|
1050
|
+
const now = Date.now();
|
|
1051
|
+
const modelCooling = Object.entries(account.modelCooldowns).filter(([, until]) => until > now).map(([modelId, until]) => ({
|
|
1052
|
+
modelId,
|
|
1053
|
+
until: new Date(until).toISOString()
|
|
1054
|
+
}));
|
|
1055
|
+
return {
|
|
1056
|
+
id: account.id,
|
|
1057
|
+
label: account.label,
|
|
1058
|
+
domain: account.credential.domain,
|
|
1059
|
+
...account.credential.expiresAtMs === 0 ? {} : { expiresAt: new Date(account.credential.expiresAtMs).toISOString() },
|
|
1060
|
+
cooling: account.cooldownUntilMs > now,
|
|
1061
|
+
...modelCooling.length === 0 ? {} : { modelCooldowns: modelCooling },
|
|
1062
|
+
rateLimitHits: account.rateLimitHits,
|
|
1063
|
+
sourcePath: account.credential.sourcePath
|
|
1064
|
+
};
|
|
1065
|
+
}),
|
|
1066
|
+
cooling: accounts.filter((account) => account.cooldownUntilMs > Date.now()).length,
|
|
1067
|
+
models: core.catalog.current().map((model) => ({
|
|
1068
|
+
id: model.id,
|
|
1069
|
+
name: model.name,
|
|
1070
|
+
multiplier: model.multiplier
|
|
1071
|
+
})),
|
|
1072
|
+
shim: { running: false }
|
|
1073
|
+
};
|
|
1074
|
+
if (asJson) console.log(JSON.stringify(status, null, 2));
|
|
1075
|
+
else console.log(`WorkBuddy XD Pool: ${status.accounts.length} account(s), ${status.cooling} cooling\n` + status.accounts.map((account) => {
|
|
1076
|
+
const flag = account.cooling ? "⏸ " : "▶ ";
|
|
1077
|
+
const hits = account.rateLimitHits > 0 ? ` (hits ${account.rateLimitHits})` : "";
|
|
1078
|
+
const modelCooling = (account.modelCooldowns ?? []).map((mc) => `\n model-cool: ${mc.modelId} until ${mc.until}`).join("");
|
|
1079
|
+
return `${flag}${account.label} [${account.domain || "cn"}]${hits}\n ${account.sourcePath}${modelCooling}`;
|
|
1080
|
+
}).join("\n"));
|
|
1081
|
+
if (withRates) console.log(`\n${formatRates(status)}`);
|
|
1082
|
+
if (withCredits) {
|
|
1083
|
+
const first = accounts.find((account) => account.cooldownUntilMs <= Date.now());
|
|
1084
|
+
if (first === void 0) console.log("\nCredits: skipped (every account is cooling down)");
|
|
1085
|
+
else try {
|
|
1086
|
+
const credits = await core.client.fetchCredits(first.credential);
|
|
1087
|
+
console.log(`\nCredits for ${first.label}: ` + JSON.stringify(credits));
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
console.log(`\nCredits for ${first.label}: query failed — ${String(error).slice(0, 200)}`);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return status.ok ? 0 : 1;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Daily check-in from the terminal.
|
|
1096
|
+
*
|
|
1097
|
+
* `checkin` with no argument reports every account's status; `checkin all`
|
|
1098
|
+
* collects wherever a reward is still available, and `checkin <label>` targets
|
|
1099
|
+
* one account. A collection is never attempted twice for the same account on
|
|
1100
|
+
* the same day — the status is re-read first and an already-collected account
|
|
1101
|
+
* is reported as such.
|
|
1102
|
+
*/
|
|
1103
|
+
async function commandCheckin(args) {
|
|
1104
|
+
const asJson = args.includes("--json");
|
|
1105
|
+
const target = args.find((arg) => !arg.startsWith("--"));
|
|
1106
|
+
const core = createCore();
|
|
1107
|
+
if ((await core.pool.scan()).length === 0) {
|
|
1108
|
+
console.error("No WorkBuddy account discovered. Sign in with the WorkBuddy desktop app first.");
|
|
1109
|
+
return 1;
|
|
1110
|
+
}
|
|
1111
|
+
const wanted = target === void 0 || target === "all" ? core.pool.list() : core.pool.list().filter((account) => account.label === target || account.id === target || account.id.startsWith(target));
|
|
1112
|
+
if (wanted.length === 0) {
|
|
1113
|
+
console.error(`No account matches "${target}". Run \`accounts\` to list labels.`);
|
|
1114
|
+
return 1;
|
|
1115
|
+
}
|
|
1116
|
+
const rows = [];
|
|
1117
|
+
for (const account of wanted) {
|
|
1118
|
+
const row = {
|
|
1119
|
+
label: account.label,
|
|
1120
|
+
active: false,
|
|
1121
|
+
alreadyCheckedIn: false,
|
|
1122
|
+
streakDays: 0
|
|
1123
|
+
};
|
|
1124
|
+
try {
|
|
1125
|
+
const status = await core.client.fetchCheckinStatus(account.credential);
|
|
1126
|
+
row.active = status.active;
|
|
1127
|
+
row.alreadyCheckedIn = status.todayCheckedIn;
|
|
1128
|
+
row.streakDays = status.streakDays;
|
|
1129
|
+
if (target !== void 0 && status.active && !status.todayCheckedIn) {
|
|
1130
|
+
const claim = await core.client.claimDailyCheckin(account.credential);
|
|
1131
|
+
row.claimed = claim.credit;
|
|
1132
|
+
row.streakDays = claim.streakDays;
|
|
1133
|
+
row.alreadyCheckedIn = true;
|
|
1134
|
+
}
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
row.error = String(error).slice(0, 200);
|
|
1137
|
+
}
|
|
1138
|
+
rows.push(row);
|
|
1139
|
+
}
|
|
1140
|
+
if (asJson) {
|
|
1141
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
1142
|
+
return rows.every((row) => row.error === void 0) ? 0 : 1;
|
|
1143
|
+
}
|
|
1144
|
+
console.log("Daily check-in:");
|
|
1145
|
+
for (const row of rows) {
|
|
1146
|
+
if (row.error !== void 0) {
|
|
1147
|
+
console.log(` ! ${row.label}: query failed — ${row.error}`);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (!row.active) {
|
|
1151
|
+
console.log(` – ${row.label}: no check-in activity`);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (row.claimed !== void 0) {
|
|
1155
|
+
console.log(` + ${row.label}: collected ${row.claimed} credit(s) (streak ${row.streakDays})`);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
console.log(row.alreadyCheckedIn ? ` ✓ ${row.label}: already checked in today (streak ${row.streakDays})` : ` · ${row.label}: not checked in today — run \`checkin all\` to collect`);
|
|
1159
|
+
}
|
|
1160
|
+
if (target === void 0) console.log("\nAdd `all` (or an account label) to collect.");
|
|
1161
|
+
return rows.every((row) => row.error === void 0) ? 0 : 1;
|
|
1162
|
+
}
|
|
1163
|
+
async function commandDoctor() {
|
|
1164
|
+
const lines = [];
|
|
1165
|
+
let healthy = true;
|
|
1166
|
+
lines.push(`dsh-workbuddy-xdpool doctor`);
|
|
1167
|
+
lines.push(` platform : ${platform()}`);
|
|
1168
|
+
lines.push(` dsh home : ${dshHome()}`);
|
|
1169
|
+
lines.push("");
|
|
1170
|
+
lines.push("Credential discovery:");
|
|
1171
|
+
for (const candidate of liveCandidates()) {
|
|
1172
|
+
const raw = await readJsonFile(candidate);
|
|
1173
|
+
const credential = raw === void 0 ? void 0 : parseWorkBuddyAuth(JSON.stringify(raw), candidate);
|
|
1174
|
+
lines.push(` ${credential === void 0 ? "✗" : "✓"} ${candidate}`);
|
|
1175
|
+
}
|
|
1176
|
+
const dirs = defaultDesktopAuthDirs();
|
|
1177
|
+
for (const dir of dirs) {
|
|
1178
|
+
const { readdir } = await import("node:fs/promises");
|
|
1179
|
+
let names = [];
|
|
1180
|
+
try {
|
|
1181
|
+
names = await readdir(dir);
|
|
1182
|
+
} catch {
|
|
1183
|
+
lines.push(` - ${dir} (absent)`);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
const snapshots = names.filter((name) => name.startsWith("workbuddy-desktop.") && name.endsWith(".info"));
|
|
1187
|
+
lines.push(` ✓ ${dir} → ${snapshots.length} snapshot(s)`);
|
|
1188
|
+
}
|
|
1189
|
+
lines.push("");
|
|
1190
|
+
const accounts = await createCore().pool.scan();
|
|
1191
|
+
lines.push(`Accounts discovered: ${accounts.length}`);
|
|
1192
|
+
if (accounts.length === 0) {
|
|
1193
|
+
healthy = false;
|
|
1194
|
+
lines.push(" ✗ none — sign in on the WorkBuddy desktop app, then run `import <key>`");
|
|
1195
|
+
}
|
|
1196
|
+
for (const account of accounts) {
|
|
1197
|
+
const state = account.cooldownUntilMs > Date.now() ? "cooling" : "ready";
|
|
1198
|
+
lines.push(` ✓ ${account.label} (${state}, hits ${account.rateLimitHits})`);
|
|
1199
|
+
}
|
|
1200
|
+
lines.push("");
|
|
1201
|
+
lines.push(`Imported snapshots: ${accountDir()}`);
|
|
1202
|
+
const { readdir: readdir2 } = await import("node:fs/promises");
|
|
1203
|
+
let imported = [];
|
|
1204
|
+
try {
|
|
1205
|
+
imported = (await readdir2(accountDir())).filter((name) => name.endsWith(".json"));
|
|
1206
|
+
} catch {}
|
|
1207
|
+
lines.push(imported.length === 0 ? " (none)" : imported.map((name) => ` ✓ ${name}`).join("\n"));
|
|
1208
|
+
console.log(lines.join("\n"));
|
|
1209
|
+
return healthy ? 0 : 1;
|
|
1210
|
+
}
|
|
1211
|
+
async function commandImport(args) {
|
|
1212
|
+
const key = args.filter((arg) => !arg.startsWith("--"))[0];
|
|
1213
|
+
if (key === void 0) {
|
|
1214
|
+
console.error("usage: dsh-workbuddy-xdpool import <key> [--force]");
|
|
1215
|
+
return 2;
|
|
1216
|
+
}
|
|
1217
|
+
const force = args.includes("--force");
|
|
1218
|
+
let source;
|
|
1219
|
+
for (const candidate of liveCandidates()) {
|
|
1220
|
+
const raw = await readJsonFile(candidate);
|
|
1221
|
+
if (raw === void 0) continue;
|
|
1222
|
+
if (parseWorkBuddyAuth(JSON.stringify(raw), candidate) !== void 0) {
|
|
1223
|
+
source = candidate;
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (source === void 0) {
|
|
1228
|
+
console.error(`No signed-in WorkBuddy desktop session found. Sign in on the desktop app first
|
|
1229
|
+
(looked in: ${liveCandidates().join(", ")})`);
|
|
1230
|
+
return 1;
|
|
1231
|
+
}
|
|
1232
|
+
const dir = accountDir();
|
|
1233
|
+
await mkdir(dir, { recursive: true });
|
|
1234
|
+
const target = snapshotPath(key, dir);
|
|
1235
|
+
const { access } = await import("node:fs/promises");
|
|
1236
|
+
let exists = false;
|
|
1237
|
+
try {
|
|
1238
|
+
await access(target);
|
|
1239
|
+
exists = true;
|
|
1240
|
+
} catch {}
|
|
1241
|
+
if (exists && !force) {
|
|
1242
|
+
console.error(`Snapshot "${key}" already exists. Re-run with --force to overwrite.`);
|
|
1243
|
+
return 1;
|
|
1244
|
+
}
|
|
1245
|
+
await copyFile(source, target);
|
|
1246
|
+
const credential = parseWorkBuddyAuth(await readFile(target, "utf8"), target);
|
|
1247
|
+
const label = credential === void 0 ? "unknown" : (credential.nickname ?? "WorkBuddy") + `#${workbuddyAccountId(credential).slice(0, 8)}`;
|
|
1248
|
+
console.log(`Imported "${key}" → ${label}\n saved: ${target}`);
|
|
1249
|
+
return 0;
|
|
1250
|
+
}
|
|
1251
|
+
async function commandRemove(args) {
|
|
1252
|
+
const key = args.filter((arg) => !arg.startsWith("--"))[0];
|
|
1253
|
+
if (key === void 0) {
|
|
1254
|
+
console.error("usage: dsh-workbuddy-xdpool remove <key>");
|
|
1255
|
+
return 2;
|
|
1256
|
+
}
|
|
1257
|
+
const target = snapshotPath(key, accountDir());
|
|
1258
|
+
const { unlink } = await import("node:fs/promises");
|
|
1259
|
+
try {
|
|
1260
|
+
await unlink(target);
|
|
1261
|
+
console.log(`Removed snapshot "${key}"`);
|
|
1262
|
+
return 0;
|
|
1263
|
+
} catch {
|
|
1264
|
+
console.error(`No snapshot named "${key}"`);
|
|
1265
|
+
return 1;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function commandLogin() {
|
|
1269
|
+
console.log([
|
|
1270
|
+
"Adding another WorkBuddy account",
|
|
1271
|
+
"",
|
|
1272
|
+
"The WorkBuddy desktop app holds one signed-in account at a time, so each",
|
|
1273
|
+
"additional account is captured as a snapshot after you switch login:",
|
|
1274
|
+
"",
|
|
1275
|
+
" 1. Open the WorkBuddy desktop app and sign in (scan the QR code).",
|
|
1276
|
+
" 2. Run: dsh plugin --profile desktop exec dsh-workbuddy-xdpool import <key>",
|
|
1277
|
+
" 3. Sign in with the next account in the desktop app.",
|
|
1278
|
+
" 4. Run the import command again with a different <key>.",
|
|
1279
|
+
" 5. Restart DSH Desktop; every imported account joins the rotation pool.",
|
|
1280
|
+
"",
|
|
1281
|
+
"Snapshots live in " + accountDir() + " and store the desktop app's tokens",
|
|
1282
|
+
"verbatim — protect that directory like a password."
|
|
1283
|
+
].join("\n"));
|
|
1284
|
+
return 0;
|
|
1285
|
+
}
|
|
1286
|
+
async function commandReset() {
|
|
1287
|
+
const core = createCore();
|
|
1288
|
+
await core.pool.scan();
|
|
1289
|
+
core.pool.resetCooldowns();
|
|
1290
|
+
console.log("Cleared all rate-limit cooldowns.");
|
|
1291
|
+
return 0;
|
|
1292
|
+
}
|
|
1293
|
+
async function commandAccounts(args) {
|
|
1294
|
+
const asJson = args.includes("--json");
|
|
1295
|
+
const accounts = await createCore().pool.scan();
|
|
1296
|
+
if (asJson) console.log(JSON.stringify(accounts.map((account) => ({
|
|
1297
|
+
id: account.id,
|
|
1298
|
+
label: account.label,
|
|
1299
|
+
cooling: account.cooldownUntilMs > Date.now(),
|
|
1300
|
+
rateLimitHits: account.rateLimitHits
|
|
1301
|
+
})), null, 2));
|
|
1302
|
+
else if (accounts.length === 0) console.log("No imported accounts. Run `import <key>` after signing in on the desktop app.");
|
|
1303
|
+
else console.log(accounts.map((account) => `${account.cooldownUntilMs > Date.now() ? "⏸" : "▶"} ${account.label}`).join("\n"));
|
|
1304
|
+
return 0;
|
|
1305
|
+
}
|
|
1306
|
+
/** Entry point; returns the process exit code. */
|
|
1307
|
+
async function main(argv) {
|
|
1308
|
+
const [command, ...rest] = argv;
|
|
1309
|
+
switch (command) {
|
|
1310
|
+
case void 0:
|
|
1311
|
+
case "help":
|
|
1312
|
+
case "--help":
|
|
1313
|
+
case "-h":
|
|
1314
|
+
console.log(usage());
|
|
1315
|
+
return 0;
|
|
1316
|
+
case "status": return commandStatus(rest);
|
|
1317
|
+
case "doctor": return commandDoctor();
|
|
1318
|
+
case "accounts": return commandAccounts(rest);
|
|
1319
|
+
case "import": return commandImport(rest);
|
|
1320
|
+
case "remove": return commandRemove(rest);
|
|
1321
|
+
case "login": return commandLogin();
|
|
1322
|
+
case "logout": return commandRemove(["default", ...rest]);
|
|
1323
|
+
case "reset": return commandReset();
|
|
1324
|
+
case "checkin": return commandCheckin(rest);
|
|
1325
|
+
default:
|
|
1326
|
+
console.error(`unknown command: ${command}\n\n${usage()}`);
|
|
1327
|
+
return 2;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
1331
|
+
console.error(error);
|
|
1332
|
+
process.exit(1);
|
|
1333
|
+
});
|
|
1334
|
+
//#endregion
|
|
1335
|
+
export { main, writeFile };
|