dsh-plugin-daily-report 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -97
- package/client.js +1851 -1716
- package/cordis.patch.yml +3 -3
- package/icon.svg +14 -14
- package/index.js +789 -770
- package/lib/ai.js +493 -493
- package/lib/collect.js +1047 -1037
- package/lib/formats.js +190 -0
- package/lib/report.js +377 -376
- package/lib/xlsx.js +416 -416
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +50 -50
package/index.js
CHANGED
|
@@ -1,770 +1,789 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host half of the daily-report plugin.
|
|
3
|
-
*
|
|
4
|
-
* It owns the local HTTP endpoints used by the Web UI and registers a
|
|
5
|
-
* `daily_report` tool so the agent can generate the same workbook.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
-
import { promises as fs } from "node:fs";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
collectReport,
|
|
15
|
-
defaultMonthRange,
|
|
16
|
-
discoverGitRepositories,
|
|
17
|
-
parseIsoDate,
|
|
18
|
-
sanitizeSettings,
|
|
19
|
-
} from "./lib/collect.js";
|
|
20
|
-
import { aiInfo, analyzeReport, regenerateDay } from "./lib/ai.js";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
res.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
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
|
-
jobs.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (
|
|
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
|
-
error
|
|
300
|
-
error.
|
|
301
|
-
throw error;
|
|
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
|
-
return
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
},
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return sendError(res,
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
return sendError(res,
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (!
|
|
739
|
-
if (
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
} catch (error) {
|
|
747
|
-
return sendError(res,
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Host half of the daily-report plugin.
|
|
3
|
+
*
|
|
4
|
+
* It owns the local HTTP endpoints used by the Web UI and registers a
|
|
5
|
+
* `daily_report` tool so the agent can generate the same workbook.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { promises as fs } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
collectReport,
|
|
15
|
+
defaultMonthRange,
|
|
16
|
+
discoverGitRepositories,
|
|
17
|
+
parseIsoDate,
|
|
18
|
+
sanitizeSettings,
|
|
19
|
+
} from "./lib/collect.js";
|
|
20
|
+
import { aiInfo, analyzeReport, regenerateDay } from "./lib/ai.js";
|
|
21
|
+
import { buildCsvReport, buildHtmlReport, buildMarkdownReport } from "./lib/formats.js";
|
|
22
|
+
import { buildReportWorkbook, reportFileName, summarizeReport, buildReportPreview, applyReportEdits } from "./lib/report.js";
|
|
23
|
+
|
|
24
|
+
export const inject = ["webServer"];
|
|
25
|
+
|
|
26
|
+
const ROUTE_STATE = "/daily-report/state";
|
|
27
|
+
const ROUTE_GENERATE = "/daily-report/generate";
|
|
28
|
+
const ROUTE_START = "/daily-report/generate/start";
|
|
29
|
+
const ROUTE_JOB = "/daily-report/job";
|
|
30
|
+
const ROUTE_JOB_FILE = "/daily-report/job/file";
|
|
31
|
+
const ROUTE_JOB_PREVIEW = "/daily-report/job/preview";
|
|
32
|
+
const ROUTE_JOB_EXPORT = "/daily-report/job/export";
|
|
33
|
+
const ROUTE_JOB_REGENERATE = "/daily-report/job/regenerate-day";
|
|
34
|
+
const ROUTE_SCAN = "/daily-report/scan";
|
|
35
|
+
const MAX_BODY = 2 * 1024 * 1024;
|
|
36
|
+
const MAX_RANGE_DAYS = 366;
|
|
37
|
+
const JOB_TTL = 60 * 60 * 1000;
|
|
38
|
+
const MAX_JOBS = 20;
|
|
39
|
+
const jobs = new Map();
|
|
40
|
+
const REPORT_FORMATS = new Set(["xlsx", "markdown", "csv", "html"]);
|
|
41
|
+
|
|
42
|
+
function normalizeFormat(value) {
|
|
43
|
+
return typeof value === "string" && REPORT_FORMATS.has(value) ? value : "xlsx";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatContentType(format) {
|
|
47
|
+
if (format === "markdown") return "text/markdown; charset=utf-8";
|
|
48
|
+
if (format === "csv") return "text/csv; charset=utf-8";
|
|
49
|
+
if (format === "html") return "text/html; charset=utf-8";
|
|
50
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildReportBuffer(result, format) {
|
|
54
|
+
if (format === "markdown") return Buffer.from(buildMarkdownReport(result), "utf8");
|
|
55
|
+
if (format === "csv") return Buffer.from(buildCsvReport(result), "utf8");
|
|
56
|
+
if (format === "html") return Buffer.from(buildHtmlReport(result), "utf8");
|
|
57
|
+
return buildReportWorkbook(result);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEFAULT_SETTINGS = {
|
|
61
|
+
git: [],
|
|
62
|
+
docs: [],
|
|
63
|
+
aiEnabled: true,
|
|
64
|
+
mineOnly: true,
|
|
65
|
+
format: "xlsx",
|
|
66
|
+
range: null,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function defaultSettings() {
|
|
70
|
+
return sanitizeSettings(DEFAULT_SETTINGS);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function homeDirectory() {
|
|
74
|
+
if (process.env.DSH_HOME && process.env.DSH_HOME.length > 0) return resolve(process.env.DSH_HOME);
|
|
75
|
+
return join(homedir(), ".dsh");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function settingsPath() {
|
|
79
|
+
return join(homeDirectory(), "daily-report", "settings.json");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function aiCachePath() {
|
|
83
|
+
return join(homeDirectory(), "daily-report", "ai-cache.json");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isLoopback(req) {
|
|
87
|
+
const address = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : "";
|
|
88
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sendJson(res, status, body) {
|
|
92
|
+
res.statusCode = status;
|
|
93
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
94
|
+
res.setHeader("cache-control", "no-store");
|
|
95
|
+
res.end(JSON.stringify(body));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sendError(res, status, code, message, details) {
|
|
99
|
+
const body = { error: { code, message } };
|
|
100
|
+
if (details !== undefined) body.error.details = details;
|
|
101
|
+
sendJson(res, status, body);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sendReportFile(res, buffer, filename, summary, contentType) {
|
|
105
|
+
const summaryHeader = Buffer.from(JSON.stringify(summary), "utf8").toString("base64");
|
|
106
|
+
const encodedName = encodeURIComponent(filename);
|
|
107
|
+
const extension = filename.includes(".") ? filename.slice(filename.lastIndexOf(".") + 1) : "xlsx";
|
|
108
|
+
res.statusCode = 200;
|
|
109
|
+
res.setHeader("content-type", contentType || "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
110
|
+
res.setHeader("content-disposition", 'attachment; filename="daily-report.' + extension + '"; filename*=UTF-8\'\'' + encodedName);
|
|
111
|
+
res.setHeader("content-length", String(buffer.length));
|
|
112
|
+
res.setHeader("cache-control", "no-store");
|
|
113
|
+
res.setHeader("x-report-filename", encodedName);
|
|
114
|
+
res.setHeader("x-report-summary", summaryHeader);
|
|
115
|
+
res.end(buffer);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeProgress(value) {
|
|
119
|
+
const raw = value && typeof value === "object" ? value : {};
|
|
120
|
+
const percent = Number(raw.percent);
|
|
121
|
+
return {
|
|
122
|
+
stage: typeof raw.stage === "string" ? raw.stage : "working",
|
|
123
|
+
percent: Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : 0,
|
|
124
|
+
indeterminate: raw.indeterminate === true,
|
|
125
|
+
message: typeof raw.message === "string" && raw.message.length > 0 ? raw.message : "正在生成日报…",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function pruneJobs() {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
for (const [id, job] of jobs) {
|
|
132
|
+
if (job.status !== "running" && now - job.updatedAt > JOB_TTL) jobs.delete(id);
|
|
133
|
+
}
|
|
134
|
+
if (jobs.size <= MAX_JOBS) return;
|
|
135
|
+
const removable = Array.from(jobs.values())
|
|
136
|
+
.filter((job) => job.status !== "running")
|
|
137
|
+
.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
138
|
+
while (jobs.size > MAX_JOBS && removable.length > 0) {
|
|
139
|
+
jobs.delete(removable.shift().id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createJob() {
|
|
144
|
+
const job = {
|
|
145
|
+
id: randomUUID(),
|
|
146
|
+
status: "running",
|
|
147
|
+
progress: normalizeProgress({ stage: "queued", percent: 1, message: "已创建生成任务,正在准备…" }),
|
|
148
|
+
createdAt: Date.now(),
|
|
149
|
+
updatedAt: Date.now(),
|
|
150
|
+
controller: new AbortController(),
|
|
151
|
+
buffer: null,
|
|
152
|
+
filename: "",
|
|
153
|
+
summary: null,
|
|
154
|
+
result: null,
|
|
155
|
+
error: null,
|
|
156
|
+
};
|
|
157
|
+
jobs.set(job.id, job);
|
|
158
|
+
return job;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function runJob(ctx, job, body) {
|
|
162
|
+
try {
|
|
163
|
+
const generated = await generateFromRequest(ctx, body, job.controller.signal, (progress) => {
|
|
164
|
+
job.progress = normalizeProgress(progress);
|
|
165
|
+
job.updatedAt = Date.now();
|
|
166
|
+
});
|
|
167
|
+
job.buffer = generated.buffer;
|
|
168
|
+
job.filename = generated.filename;
|
|
169
|
+
job.summary = summarizeReport(generated.result);
|
|
170
|
+
job.result = generated.result;
|
|
171
|
+
job.status = "done";
|
|
172
|
+
job.progress = normalizeProgress({ stage: "done", percent: 100, message: "日报生成完成,正在准备下载…" });
|
|
173
|
+
job.updatedAt = Date.now();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
job.updatedAt = Date.now();
|
|
176
|
+
if (job.controller.signal.aborted) {
|
|
177
|
+
job.status = "cancelled";
|
|
178
|
+
job.error = { code: "cancelled", message: "生成已取消。" };
|
|
179
|
+
job.progress = normalizeProgress({ stage: "cancelled", percent: job.progress.percent, message: "生成已取消。" });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
job.status = "failed";
|
|
183
|
+
job.error = {
|
|
184
|
+
code: error && error.code ? error.code : "generate-failed",
|
|
185
|
+
message: (error && error.message) || "生成日报失败。",
|
|
186
|
+
...(error && error.details !== undefined ? { details: error.details } : {}),
|
|
187
|
+
};
|
|
188
|
+
job.progress = normalizeProgress({ stage: "failed", percent: job.progress.percent, message: "生成失败。" });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function readBody(req) {
|
|
193
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
194
|
+
let size = 0;
|
|
195
|
+
let tooLarge = false;
|
|
196
|
+
const chunks = [];
|
|
197
|
+
req.on("data", (chunk) => {
|
|
198
|
+
if (tooLarge) return;
|
|
199
|
+
size += chunk.length;
|
|
200
|
+
if (size > MAX_BODY) {
|
|
201
|
+
tooLarge = true;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
chunks.push(chunk);
|
|
205
|
+
});
|
|
206
|
+
req.on("end", () => {
|
|
207
|
+
if (tooLarge) {
|
|
208
|
+
rejectBody(new Error("body-too-large"));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
resolveBody(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
213
|
+
} catch {
|
|
214
|
+
rejectBody(new Error("invalid-json"));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
req.on("error", rejectBody);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function readSettings() {
|
|
222
|
+
try {
|
|
223
|
+
const text = await fs.readFile(settingsPath(), "utf8");
|
|
224
|
+
const parsed = JSON.parse(text);
|
|
225
|
+
return { settings: sanitizeSettings(parsed && parsed.settings ? parsed.settings : parsed), saved: true, warning: "" };
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error && error.code === "ENOENT") {
|
|
228
|
+
return { settings: defaultSettings(), saved: false, warning: "" };
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
settings: defaultSettings(),
|
|
232
|
+
saved: false,
|
|
233
|
+
warning: "设置文件无法读取,已使用默认数据源。",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function writeSettings(settings) {
|
|
239
|
+
const file = settingsPath();
|
|
240
|
+
await fs.mkdir(dirname(file), { recursive: true });
|
|
241
|
+
const temporary = file + ".tmp";
|
|
242
|
+
await fs.writeFile(temporary, JSON.stringify({ version: 1, settings }, null, 2), "utf8");
|
|
243
|
+
await fs.rename(temporary, file);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resolveCwd(ctx, sessionId) {
|
|
247
|
+
try {
|
|
248
|
+
const sessions = ctx.get("sessions");
|
|
249
|
+
const workingDirectory = ctx.get("workingDirectory");
|
|
250
|
+
if (sessions && workingDirectory && typeof sessionId === "string" && sessionId.length > 0) {
|
|
251
|
+
const session = sessions.get(sessionId);
|
|
252
|
+
if (session) {
|
|
253
|
+
const cwd = workingDirectory.get(session);
|
|
254
|
+
if (typeof cwd === "string" && cwd.length > 0) return cwd;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
258
|
+
const registry = ctx.get("workspaceRegistry");
|
|
259
|
+
if (registry) {
|
|
260
|
+
const workspaces = registry.list();
|
|
261
|
+
const first = Array.isArray(workspaces) ? workspaces[0] : undefined;
|
|
262
|
+
if (first && typeof first.path === "string" && first.path.length > 0) return first.path;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (workingDirectory && typeof workingDirectory.defaultDirectory === "string" && workingDirectory.defaultDirectory.length > 0) {
|
|
266
|
+
return workingDirectory.defaultDirectory;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
// Fall through to the process directory.
|
|
270
|
+
}
|
|
271
|
+
return process.cwd();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function validateRange(start, end) {
|
|
275
|
+
const first = parseIsoDate(start);
|
|
276
|
+
const last = parseIsoDate(end);
|
|
277
|
+
if (!first || !last) return "invalid-range";
|
|
278
|
+
if (first > last) return "invalid-range";
|
|
279
|
+
const days = Math.round((last.getTime() - first.getTime()) / 86400000) + 1;
|
|
280
|
+
if (days > MAX_RANGE_DAYS) return "range-too-long";
|
|
281
|
+
return "";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function generateFromRequest(ctx, body, signal, onProgress) {
|
|
285
|
+
const report = (progress) => {
|
|
286
|
+
if (typeof onProgress === "function") onProgress(progress);
|
|
287
|
+
};
|
|
288
|
+
report({ stage: "validate", percent: 2, message: "正在校验生成参数…" });
|
|
289
|
+
const start = typeof body.start === "string" ? body.start : "";
|
|
290
|
+
const end = typeof body.end === "string" ? body.end : "";
|
|
291
|
+
const rangeError = validateRange(start, end);
|
|
292
|
+
if (rangeError) {
|
|
293
|
+
const error = new Error(rangeError === "range-too-long" ? "时间范围最长支持 366 天。" : "日期范围无效,请检查开始和结束日期。");
|
|
294
|
+
error.code = rangeError;
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
const settings = sanitizeSettings(body.settings);
|
|
298
|
+
if (settings.git.length === 0 && settings.docs.length === 0) {
|
|
299
|
+
const error = new Error("请至少添加一个 Git 仓库或本地文档。");
|
|
300
|
+
error.code = "no-sources";
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
const locale = body.locale === "en" ? "en" : "zh";
|
|
304
|
+
const requestedCwd = typeof body.cwd === "string" ? body.cwd.trim() : "";
|
|
305
|
+
const cwd = requestedCwd.length > 0 && isAbsolute(requestedCwd) ? resolve(requestedCwd) : resolveCwd(ctx, body.sessionId);
|
|
306
|
+
const wantAi = body.ai === true;
|
|
307
|
+
const result = await collectReport({
|
|
308
|
+
settings,
|
|
309
|
+
start,
|
|
310
|
+
end,
|
|
311
|
+
cwd,
|
|
312
|
+
homeDir: homeDirectory(),
|
|
313
|
+
locale,
|
|
314
|
+
signal,
|
|
315
|
+
includeDiff: wantAi,
|
|
316
|
+
mineOnly: settings.mineOnly === true,
|
|
317
|
+
onProgress: report,
|
|
318
|
+
now: new Date(),
|
|
319
|
+
});
|
|
320
|
+
if (result.stats.sources > 0 && result.stats.okSources === 0) {
|
|
321
|
+
const error = new Error("所有数据源都读取失败,请检查地址、分支或文件路径。");
|
|
322
|
+
error.code = "all-sources-failed";
|
|
323
|
+
error.details = result.sources;
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
if (wantAi) {
|
|
327
|
+
result.ai = await analyzeReport(ctx, result, { signal, locale, onProgress: report, cacheFile: aiCachePath() });
|
|
328
|
+
if (!result.ai.ok) {
|
|
329
|
+
result.warnings.push("AI 分析未完成:" + (result.ai.error || "未知错误"));
|
|
330
|
+
} else {
|
|
331
|
+
for (const warning of result.ai.warnings || []) result.warnings.push("AI 分析:" + warning);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
report({ stage: "build", percent: 97, message: "正在生成 XLSX 工作簿…" });
|
|
335
|
+
const buffer = buildReportWorkbook(result);
|
|
336
|
+
report({ stage: "done", percent: 100, message: "工作簿已生成,正在准备下载…" });
|
|
337
|
+
return { result, buffer, filename: reportFileName(start, end) };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function registerRoute(ctx, path, handler, label) {
|
|
341
|
+
const dispose = ctx.webServer.register({ kind: "exact", path, handler });
|
|
342
|
+
ctx.effect(() => dispose, label);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function ensureAbsolute(cwd, value) {
|
|
346
|
+
if (typeof value !== "string" || value.trim().length === 0) return "";
|
|
347
|
+
return isAbsolute(value) ? resolve(value) : resolve(cwd, value);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderToolResult(value) {
|
|
351
|
+
const lines = [
|
|
352
|
+
"日报已生成:" + value.outputFile,
|
|
353
|
+
"范围:" + value.range.start + " ~ " + value.range.end,
|
|
354
|
+
"Git 提交:" + value.stats.commits + " 条;文档:" + value.stats.docs + " 份;有记录天数:" + value.stats.activeDays + " 天",
|
|
355
|
+
];
|
|
356
|
+
if (value.ai && value.ai.ok) {
|
|
357
|
+
lines.push("AI 分析:" + (value.ai.overall || "见 XLSX 的 AI分析 工作表"));
|
|
358
|
+
} else if (value.ai && value.ai.error) {
|
|
359
|
+
lines.push("AI 分析未完成:" + value.ai.error);
|
|
360
|
+
}
|
|
361
|
+
if (value.warnings && value.warnings.length > 0) {
|
|
362
|
+
lines.push("提示:");
|
|
363
|
+
for (const warning of value.warnings.slice(0, 10)) lines.push("- " + warning);
|
|
364
|
+
if (value.warnings.length > 10) lines.push("- …共 " + value.warnings.length + " 条提示");
|
|
365
|
+
}
|
|
366
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function registerTool(ctx) {
|
|
370
|
+
const tools = ctx.get("tools");
|
|
371
|
+
if (!tools) return;
|
|
372
|
+
const dispose = tools.register({
|
|
373
|
+
name: "daily_report",
|
|
374
|
+
description:
|
|
375
|
+
"Generate a daily report workbook (XLSX) from one or more Git repositories/branches and local document paths. The date range defaults to the current month. Set ai=true to analyze commit diffs, commit messages and document excerpts with the current default model. Use this when the user asks for a daily, weekly, or monthly work summary exported to Excel.",
|
|
376
|
+
parameters: {
|
|
377
|
+
type: "object",
|
|
378
|
+
additionalProperties: false,
|
|
379
|
+
properties: {
|
|
380
|
+
start: { type: "string", description: "Start date, inclusive, in YYYY-MM-DD format. Defaults to the first day of the current month." },
|
|
381
|
+
end: { type: "string", description: "End date, inclusive, in YYYY-MM-DD format. Defaults to the last day of the current month." },
|
|
382
|
+
git: {
|
|
383
|
+
type: "array",
|
|
384
|
+
description: "Git sources. When omitted, the saved plugin configuration is used.",
|
|
385
|
+
items: {
|
|
386
|
+
type: "object",
|
|
387
|
+
additionalProperties: false,
|
|
388
|
+
properties: {
|
|
389
|
+
location: { type: "string", description: "Remote URL or local repository path." },
|
|
390
|
+
branch: { type: "string", description: "Branch name. Empty means the default branch." },
|
|
391
|
+
label: { type: "string", description: "Display name in the report." },
|
|
392
|
+
},
|
|
393
|
+
required: ["location"],
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
docs: {
|
|
397
|
+
type: "array",
|
|
398
|
+
description: "Local document files or directories. When omitted, the saved plugin configuration is used.",
|
|
399
|
+
items: { type: "string" },
|
|
400
|
+
},
|
|
401
|
+
outputPath: { type: "string", description: "Optional output path. Defaults to the session working directory." },
|
|
402
|
+
ai: { type: "boolean", description: "Analyze commit diffs, commit messages and document excerpts with the current default model, then add per-day AI summaries to the workbook." },
|
|
403
|
+
format: { type: "string", enum: ["xlsx", "markdown", "csv", "html"], description: "Output format. Defaults to xlsx." },
|
|
404
|
+
locale: { type: "string", enum: ["zh", "en"], description: "Report language. Defaults to zh." },
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
output: {
|
|
408
|
+
schema: {
|
|
409
|
+
type: "object",
|
|
410
|
+
properties: {
|
|
411
|
+
outputFile: { type: "string" },
|
|
412
|
+
range: {
|
|
413
|
+
type: "object",
|
|
414
|
+
properties: {
|
|
415
|
+
start: { type: "string" },
|
|
416
|
+
end: { type: "string" },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
stats: {
|
|
420
|
+
type: "object",
|
|
421
|
+
properties: {
|
|
422
|
+
days: { type: "number" },
|
|
423
|
+
activeDays: { type: "number" },
|
|
424
|
+
commits: { type: "number" },
|
|
425
|
+
docs: { type: "number" },
|
|
426
|
+
sources: { type: "number" },
|
|
427
|
+
okSources: { type: "number" },
|
|
428
|
+
failedSources: { type: "number" },
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
sources: { type: "array", items: { type: "object" } },
|
|
432
|
+
warnings: { type: "array", items: { type: "string" } },
|
|
433
|
+
ai: {
|
|
434
|
+
type: "object",
|
|
435
|
+
properties: {
|
|
436
|
+
requested: { type: "boolean" },
|
|
437
|
+
ok: { type: "boolean" },
|
|
438
|
+
provider: { type: "string" },
|
|
439
|
+
model: { type: "string" },
|
|
440
|
+
overall: { type: "string" },
|
|
441
|
+
error: { type: "string" },
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
render(_args, value) {
|
|
447
|
+
return renderToolResult(value);
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
async execute(args, exec) {
|
|
451
|
+
const input = args && typeof args === "object" ? args : {};
|
|
452
|
+
const saved = await readSettings();
|
|
453
|
+
const settings = {
|
|
454
|
+
git: Array.isArray(input.git) ? sanitizeSettings({ git: input.git }).git : saved.settings.git,
|
|
455
|
+
docs: Array.isArray(input.docs) ? sanitizeSettings({ docs: input.docs }).docs : saved.settings.docs,
|
|
456
|
+
};
|
|
457
|
+
if (settings.git.length === 0 && settings.docs.length === 0) {
|
|
458
|
+
throw new Error("没有配置数据源。请先在日报页面添加 Git 仓库或本地文档。");
|
|
459
|
+
}
|
|
460
|
+
const defaults = defaultMonthRange(new Date());
|
|
461
|
+
const start = typeof input.start === "string" && input.start.length > 0 ? input.start : defaults.start;
|
|
462
|
+
const end = typeof input.end === "string" && input.end.length > 0 ? input.end : defaults.end;
|
|
463
|
+
const rangeError = validateRange(start, end);
|
|
464
|
+
if (rangeError) {
|
|
465
|
+
throw new Error(rangeError === "range-too-long" ? "时间范围最长支持 366 天。" : "日期范围无效。");
|
|
466
|
+
}
|
|
467
|
+
let cwd = process.cwd();
|
|
468
|
+
const workingDirectory = ctx.get("workingDirectory");
|
|
469
|
+
if (workingDirectory && exec.agent) {
|
|
470
|
+
try {
|
|
471
|
+
cwd = await workingDirectory.ensure(exec.agent, exec.signal);
|
|
472
|
+
} catch {
|
|
473
|
+
cwd = process.cwd();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const locale = input.locale === "en" ? "en" : "zh";
|
|
477
|
+
const wantAi = input.ai === true;
|
|
478
|
+
const result = await collectReport({
|
|
479
|
+
settings,
|
|
480
|
+
start,
|
|
481
|
+
end,
|
|
482
|
+
cwd,
|
|
483
|
+
homeDir: homeDirectory(),
|
|
484
|
+
locale,
|
|
485
|
+
signal: exec.signal,
|
|
486
|
+
includeDiff: wantAi,
|
|
487
|
+
mineOnly: settings.mineOnly === true,
|
|
488
|
+
now: new Date(),
|
|
489
|
+
});
|
|
490
|
+
if (result.stats.sources > 0 && result.stats.okSources === 0) {
|
|
491
|
+
const error = new Error("所有数据源都读取失败,请检查地址、分支或文件路径。");
|
|
492
|
+
error.details = result.sources;
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
if (wantAi) {
|
|
496
|
+
result.ai = await analyzeReport(ctx, result, { signal: exec.signal, locale, cacheFile: aiCachePath() });
|
|
497
|
+
if (!result.ai.ok) {
|
|
498
|
+
result.warnings.push("AI 分析未完成:" + (result.ai.error || "未知错误"));
|
|
499
|
+
} else {
|
|
500
|
+
for (const warning of result.ai.warnings || []) result.warnings.push("AI 分析:" + warning);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const filename = reportFileName(start, end, normalizeFormat(input.format));
|
|
504
|
+
const target = ensureAbsolute(cwd, input.outputPath) || join(cwd, filename);
|
|
505
|
+
const buffer = buildReportBuffer(result, normalizeFormat(input.format));
|
|
506
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
507
|
+
await fs.writeFile(target, buffer);
|
|
508
|
+
return {
|
|
509
|
+
outputFile: target,
|
|
510
|
+
range: result.range,
|
|
511
|
+
stats: result.stats,
|
|
512
|
+
sources: result.sources,
|
|
513
|
+
warnings: result.warnings,
|
|
514
|
+
ai: result.ai
|
|
515
|
+
? {
|
|
516
|
+
requested: true,
|
|
517
|
+
ok: result.ai.ok === true,
|
|
518
|
+
provider: result.ai.provider || "",
|
|
519
|
+
model: result.ai.model || "",
|
|
520
|
+
overall: result.ai.overall || "",
|
|
521
|
+
error: result.ai.error || "",
|
|
522
|
+
}
|
|
523
|
+
: null,
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
isConcurrencySafe() {
|
|
527
|
+
return false;
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
ctx.effect(() => dispose, "daily-report: agent tool");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function apply(ctx) {
|
|
534
|
+
registerRoute(
|
|
535
|
+
ctx,
|
|
536
|
+
ROUTE_STATE,
|
|
537
|
+
async (req, res) => {
|
|
538
|
+
if (!isLoopback(req)) return sendError(res, 403, "forbidden", "只允许本机访问。");
|
|
539
|
+
if (req.method === "GET") {
|
|
540
|
+
const saved = await readSettings();
|
|
541
|
+
return sendJson(res, 200, {
|
|
542
|
+
settings: saved.settings,
|
|
543
|
+
saved: saved.saved,
|
|
544
|
+
warning: saved.warning,
|
|
545
|
+
defaults: defaultMonthRange(new Date()),
|
|
546
|
+
ai: aiInfo(ctx),
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (req.method === "PUT") {
|
|
550
|
+
let body;
|
|
551
|
+
try {
|
|
552
|
+
body = await readBody(req);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
return sendError(res, error.message === "body-too-large" ? 413 : 400, error.message, "请求内容无效。");
|
|
555
|
+
}
|
|
556
|
+
const settings = sanitizeSettings(body && body.settings ? body.settings : body);
|
|
557
|
+
try {
|
|
558
|
+
await writeSettings(settings);
|
|
559
|
+
return sendJson(res, 200, { ok: true, settings });
|
|
560
|
+
} catch (error) {
|
|
561
|
+
return sendError(res, 500, "save-failed", "保存设置失败:" + (error.message || "未知错误"));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
res.setHeader("allow", "GET, PUT");
|
|
565
|
+
return sendError(res, 405, "method-not-allowed", "请求方法不支持。");
|
|
566
|
+
},
|
|
567
|
+
"daily-report: state route",
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
registerRoute(
|
|
571
|
+
ctx,
|
|
572
|
+
ROUTE_SCAN,
|
|
573
|
+
async (req, res) => {
|
|
574
|
+
if (!isLoopback(req)) return sendError(res, 403, "forbidden", "只允许本机访问。");
|
|
575
|
+
if (req.method !== "POST") {
|
|
576
|
+
res.setHeader("allow", "POST");
|
|
577
|
+
return sendError(res, 405, "method-not-allowed", "请求方法不支持。");
|
|
578
|
+
}
|
|
579
|
+
let body;
|
|
580
|
+
try {
|
|
581
|
+
body = await readBody(req);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
return sendError(res, error.message === "body-too-large" ? 413 : 400, error.message, "请求内容无效。");
|
|
584
|
+
}
|
|
585
|
+
const directory = typeof body.directory === "string" ? body.directory.trim() : "";
|
|
586
|
+
if (directory.length === 0) return sendError(res, 400, "invalid-directory", "请填写要扫描的目录。");
|
|
587
|
+
const base = typeof body.cwd === "string" && body.cwd.trim().length > 0 && isAbsolute(body.cwd.trim())
|
|
588
|
+
? resolve(body.cwd.trim())
|
|
589
|
+
: resolveCwd(ctx, "");
|
|
590
|
+
try {
|
|
591
|
+
const found = await discoverGitRepositories(directory, { cwd: base });
|
|
592
|
+
return sendJson(res, 200, found);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
return sendError(res, 400, "scan-failed", error.message || "扫描目录失败。");
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
"daily-report: scan route",
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
registerRoute(
|
|
601
|
+
ctx,
|
|
602
|
+
ROUTE_GENERATE,
|
|
603
|
+
async (req, res) => {
|
|
604
|
+
if (!isLoopback(req)) return sendError(res, 403, "forbidden", "只允许本机访问。");
|
|
605
|
+
if (req.method !== "POST") {
|
|
606
|
+
res.setHeader("allow", "POST");
|
|
607
|
+
return sendError(res, 405, "method-not-allowed", "请求方法不支持。");
|
|
608
|
+
}
|
|
609
|
+
let body;
|
|
610
|
+
try {
|
|
611
|
+
body = await readBody(req);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
return sendError(res, error.message === "body-too-large" ? 413 : 400, error.message, "请求内容无效。");
|
|
614
|
+
}
|
|
615
|
+
const controller = new AbortController();
|
|
616
|
+
const abort = () => controller.abort(new Error("客户端已取消"));
|
|
617
|
+
req.on("aborted", abort);
|
|
618
|
+
try {
|
|
619
|
+
const generated = await generateFromRequest(ctx, body || {}, controller.signal);
|
|
620
|
+
sendReportFile(res, generated.buffer, generated.filename, summarizeReport(generated.result));
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (controller.signal.aborted) return;
|
|
623
|
+
const code = error && error.code ? error.code : "generate-failed";
|
|
624
|
+
const status = code === "no-sources" || code === "invalid-range" || code === "range-too-long" ? 400 : code === "all-sources-failed" ? 422 : 500;
|
|
625
|
+
sendError(res, status, code, error.message || "生成日报失败。", error.details);
|
|
626
|
+
} finally {
|
|
627
|
+
req.off("aborted", abort);
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
"daily-report: generate route",
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
registerRoute(
|
|
634
|
+
ctx,
|
|
635
|
+
ROUTE_START,
|
|
636
|
+
async (req, res) => {
|
|
637
|
+
if (!isLoopback(req)) return sendError(res, 403, "forbidden", "只允许本机访问。");
|
|
638
|
+
if (req.method !== "POST") {
|
|
639
|
+
res.setHeader("allow", "POST");
|
|
640
|
+
return sendError(res, 405, "method-not-allowed", "请求方法不支持。");
|
|
641
|
+
}
|
|
642
|
+
let body;
|
|
643
|
+
try {
|
|
644
|
+
body = await readBody(req);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
return sendError(res, error.message === "body-too-large" ? 413 : 400, error.message, "请求内容无效。");
|
|
647
|
+
}
|
|
648
|
+
pruneJobs();
|
|
649
|
+
const job = createJob();
|
|
650
|
+
runJob(ctx, job, body || {});
|
|
651
|
+
return sendJson(res, 202, { jobId: job.id });
|
|
652
|
+
},
|
|
653
|
+
"daily-report: start route",
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
registerRoute(
|
|
657
|
+
ctx,
|
|
658
|
+
ROUTE_JOB,
|
|
659
|
+
async (req, res) => {
|
|
660
|
+
if (!isLoopback(req)) return sendError(res, 403, "forbidden", "只允许本机访问。");
|
|
661
|
+
if (req.method !== "GET") {
|
|
662
|
+
res.setHeader("allow", "GET");
|
|
663
|
+
return sendError(res, 405, "method-not-allowed", "请求方法不支持。");
|
|
664
|
+
}
|
|
665
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
666
|
+
const jobId = url.searchParams.get("jobId") || "";
|
|
667
|
+
const job = jobs.get(jobId);
|
|
668
|
+
if (!job) return sendError(res, 404, "job-not-found", "生成任务不存在或已过期。");
|
|
669
|
+
return sendJson(res, 200, {
|
|
670
|
+
jobId: job.id,
|
|
671
|
+
status: job.status,
|
|
672
|
+
progress: job.progress,
|
|
673
|
+
error: job.error,
|
|
674
|
+
filename: job.filename,
|
|
675
|
+
size: job.buffer ? job.buffer.length : 0,
|
|
676
|
+
summary: job.summary,
|
|
677
|
+
});
|
|
678
|
+
},
|
|
679
|
+
"daily-report: job status route",
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
registerRoute(
|
|
683
|
+
ctx,
|
|
684
|
+
ROUTE_JOB_FILE,
|
|
685
|
+
async (req, res) => {
|
|
686
|
+
if (!isLoopback(req)) return sendError(res, 403, "forbidden", "只允许本机访问。");
|
|
687
|
+
if (req.method !== "GET") {
|
|
688
|
+
res.setHeader("allow", "GET");
|
|
689
|
+
return sendError(res, 405, "method-not-allowed", "请求方法不支持。");
|
|
690
|
+
}
|
|
691
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
692
|
+
const jobId = url.searchParams.get("jobId") || "";
|
|
693
|
+
const job = jobs.get(jobId);
|
|
694
|
+
if (!job) return sendError(res, 404, "job-not-found", "生成任务不存在或已过期。");
|
|
695
|
+
if (job.status !== "done" || !job.buffer) {
|
|
696
|
+
return sendError(res, 409, "job-not-ready", "生成任务尚未完成。");
|
|
697
|
+
}
|
|
698
|
+
return sendReportFile(res, job.buffer, job.filename, job.summary);
|
|
699
|
+
},
|
|
700
|
+
"daily-report: job file route",
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
registerRoute(
|
|
704
|
+
ctx,
|
|
705
|
+
ROUTE_JOB_PREVIEW,
|
|
706
|
+
async (req, res) => {
|
|
707
|
+
if (!isLoopback(req)) return sendError(res, 403, "forbidden", "只允许本机访问。");
|
|
708
|
+
if (req.method !== "GET") {
|
|
709
|
+
res.setHeader("allow", "GET");
|
|
710
|
+
return sendError(res, 405, "method-not-allowed", "请求方法不支持。");
|
|
711
|
+
}
|
|
712
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
713
|
+
const jobId = url.searchParams.get("jobId") || "";
|
|
714
|
+
const job = jobs.get(jobId);
|
|
715
|
+
if (!job) return sendError(res, 404, "job-not-found", "生成任务不存在或已过期。");
|
|
716
|
+
if (job.status !== "done" || !job.result) return sendError(res, 409, "job-not-ready", "生成任务尚未完成。");
|
|
717
|
+
return sendJson(res, 200, { jobId: job.id, ...buildReportPreview(job.result) });
|
|
718
|
+
},
|
|
719
|
+
"daily-report: preview route",
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
registerRoute(
|
|
723
|
+
ctx,
|
|
724
|
+
ROUTE_JOB_EXPORT,
|
|
725
|
+
async (req, res) => {
|
|
726
|
+
if (!isLoopback(req)) return sendError(res, 403, "forbidden", "只允许本机访问。");
|
|
727
|
+
if (req.method !== "POST") {
|
|
728
|
+
res.setHeader("allow", "POST");
|
|
729
|
+
return sendError(res, 405, "method-not-allowed", "请求方法不支持。");
|
|
730
|
+
}
|
|
731
|
+
let body;
|
|
732
|
+
try {
|
|
733
|
+
body = await readBody(req);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
return sendError(res, error.message === "body-too-large" ? 413 : 400, error.message, "请求内容无效。");
|
|
736
|
+
}
|
|
737
|
+
const job = jobs.get(typeof body.jobId === "string" ? body.jobId : "");
|
|
738
|
+
if (!job) return sendError(res, 404, "job-not-found", "生成任务不存在或已过期。");
|
|
739
|
+
if (job.status !== "done" || !job.result) return sendError(res, 409, "job-not-ready", "生成任务尚未完成。");
|
|
740
|
+
try {
|
|
741
|
+
const edited = applyReportEdits(job.result, body.edits || {});
|
|
742
|
+
const format = normalizeFormat(body.format);
|
|
743
|
+
const buffer = buildReportBuffer(edited, format);
|
|
744
|
+
const filename = reportFileName(edited.range.start, edited.range.end, format);
|
|
745
|
+
return sendReportFile(res, buffer, filename, summarizeReport(edited), formatContentType(format));
|
|
746
|
+
} catch (error) {
|
|
747
|
+
return sendError(res, 400, "export-failed", error.message || "导出日报失败。");
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
"daily-report: preview export route",
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
registerRoute(
|
|
754
|
+
ctx,
|
|
755
|
+
ROUTE_JOB_REGENERATE,
|
|
756
|
+
async (req, res) => {
|
|
757
|
+
if (!isLoopback(req)) return sendError(res, 403, "forbidden", "只允许本机访问。");
|
|
758
|
+
if (req.method !== "POST") {
|
|
759
|
+
res.setHeader("allow", "POST");
|
|
760
|
+
return sendError(res, 405, "method-not-allowed", "请求方法不支持。");
|
|
761
|
+
}
|
|
762
|
+
let body;
|
|
763
|
+
try {
|
|
764
|
+
body = await readBody(req);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
return sendError(res, error.message === "body-too-large" ? 413 : 400, error.message, "请求内容无效。");
|
|
767
|
+
}
|
|
768
|
+
const job = jobs.get(typeof body.jobId === "string" ? body.jobId : "");
|
|
769
|
+
if (!job) return sendError(res, 404, "job-not-found", "生成任务不存在或已过期。");
|
|
770
|
+
if (job.status !== "done" || !job.result) return sendError(res, 409, "job-not-ready", "生成任务尚未完成。");
|
|
771
|
+
const date = typeof body.date === "string" ? body.date : "";
|
|
772
|
+
try {
|
|
773
|
+
const regenerated = await regenerateDay(ctx, job.result, date, {
|
|
774
|
+
locale: job.result.locale,
|
|
775
|
+
manualItems: Array.isArray(body.manualItems) ? body.manualItems : [],
|
|
776
|
+
cacheFile: aiCachePath(),
|
|
777
|
+
});
|
|
778
|
+
return sendJson(res, 200, regenerated);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
return sendError(res, 400, "regenerate-failed", error.message || "重新分析失败。");
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
"daily-report: preview regenerate route",
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
registerTool(ctx);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|