wukong-gitlog-cli 1.0.35 → 1.0.37

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.
@@ -7,6 +7,7 @@ import ora from 'ora'
7
7
  import path from 'path'
8
8
  import { fileURLToPath } from 'url'
9
9
 
10
+ import { parseOptions } from './cli/parseOptions.mjs'
10
11
  // eslint-disable-next-line no-unused-vars
11
12
  import { CLI_NAME } from './constants/index.mjs'
12
13
  import {
@@ -15,23 +16,27 @@ import {
15
16
  exportExcelPerPeriodSheets
16
17
  } from './excel.mjs'
17
18
  import { getGitLogsFast } from './git.mjs'
19
+ import { handleServe } from './handlers/handleServe.mjs'
18
20
  import { renderAuthorChangesJson } from './json.mjs'
21
+ import { setConfig } from './lib/configStore.mjs'
22
+ import { createOvertimeStats } from './overtime/createOvertimeStats.mjs'
19
23
  import {
20
- analyzeOvertime,
21
24
  renderOvertimeCsv,
22
25
  renderOvertimeTab,
23
26
  renderOvertimeText
24
- } from './overtime.mjs'
27
+ } from './overtime/overtime.mjs'
25
28
  import { renderAuthorMapText } from './renderAuthorMapText.mjs'
26
29
  import { startServer } from './server.mjs'
27
30
  import { renderChangedLinesText, renderText } from './text.mjs'
28
31
  import { checkUpdateWithPatch } from './utils/checkUpdate.mjs'
32
+ import { handleSuccess } from './utils/handleSuccess.mjs'
29
33
  import {
30
34
  groupRecords,
31
35
  outputFilePath,
32
36
  writeJSON,
33
37
  writeTextFile
34
38
  } from './utils/index.mjs'
39
+ import { logDev } from './utils/logDev.mjs'
35
40
  import { showVersionInfo } from './utils/showVersionInfo.mjs'
36
41
 
37
42
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -78,6 +83,8 @@ export function getWeekRange(periodStr) {
78
83
  }
79
84
 
80
85
  const main = async () => {
86
+ const startTime = performance.now()
87
+
81
88
  const program = new Command()
82
89
 
83
90
  program
@@ -90,6 +97,7 @@ const main = async () => {
90
97
  .option('--until <date>', '结束日期')
91
98
  .option('--limit <n>', '限制数量', parseInt)
92
99
  .option('--no-merges', '不包含 merge commit')
100
+ .option('--export', '导出统计数据')
93
101
  .option('--json', '输出 JSON')
94
102
  .option('--format <type>', '输出格式: text | excel | json', 'text')
95
103
  .option('--group-by <type>', '按日期分组: day | month | week')
@@ -171,6 +179,7 @@ const main = async () => {
171
179
  (v) => parseInt(v, 10),
172
180
  3000
173
181
  )
182
+ .option('--debug', 'enable debug logs')
174
183
  .option(
175
184
  '--serve-only',
176
185
  '仅启动 web 服务,不导出或分析数据(使用 output-wukong/data 中已有的数据)'
@@ -179,6 +188,14 @@ const main = async () => {
179
188
  .parse()
180
189
 
181
190
  const opts = program.opts()
191
+
192
+ const config = parseOptions(opts)
193
+
194
+ // ❗只创建一次缓存实例
195
+ const getOvertimeStats = createOvertimeStats(config)
196
+
197
+ setConfig('debug', opts.debug === true)
198
+
182
199
  // compute output directory root early (so serve-only can use it)
183
200
  const outDir = opts.outParent
184
201
  ? path.resolve(process.cwd(), '..', 'output-wukong')
@@ -308,16 +325,14 @@ const main = async () => {
308
325
  // --- 分组 ---
309
326
  const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null
310
327
 
328
+ // If serve mode is enabled, write data modules and launch the web server
329
+ if (opts.serve) {
330
+ handleServe({ opts, outDir, records, getOvertimeStats })
331
+ }
332
+
311
333
  // --- Overtime analysis ---
312
334
  if (opts.overtime) {
313
- const stats = analyzeOvertime(records, {
314
- startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
315
- endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
316
- lunchStart:
317
- opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
318
- lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
319
- country: opts.country || 'CN'
320
- })
335
+ const stats = getOvertimeStats(records)
321
336
  // Output to console
322
337
  console.log('\n--- Overtime analysis ---\n')
323
338
  console.log(renderOvertimeText(stats))
@@ -325,13 +340,13 @@ const main = async () => {
325
340
  const authorMapText = renderAuthorMapText(authorMap)
326
341
  console.log('\n Developers:\n', authorMapText, '\n')
327
342
  writeTextFile(outputFilePath('authors.text', outDir), authorMapText)
328
-
343
+
329
344
  // if user requested json format, write stats to file
330
345
  if (opts.json || opts.format === 'json') {
331
346
  const file = opts.out || 'overtime.json'
332
347
  const filepath = outputFilePath(file, outDir)
333
348
  writeJSON(filepath, stats)
334
- console.log(chalk.green(`overtime JSON 已导出: ${filepath}`))
349
+ logDev(`overtime JSON 已导出: ${filepath}`)
335
350
  }
336
351
  // Always write human readable overtime text to file (default: overtime.txt)
337
352
  const outBase = opts.out
@@ -348,218 +363,9 @@ const main = async () => {
348
363
  const overtimeCsvFileName = `overtime_${outBase}.csv`
349
364
  const overtimeCsvFile = outputFilePath(overtimeCsvFileName, outDir)
350
365
  writeTextFile(overtimeCsvFile, renderOvertimeCsv(stats))
351
- console.log(chalk.green(`Overtime text 已导出: ${overtimeFile}`))
352
- console.log(chalk.green(`Overtime table (tabs) 已导出: ${overtimeTabFile}`))
353
- console.log(chalk.green(`Overtime CSV 已导出: ${overtimeCsvFile}`))
354
-
355
- // If serve mode is enabled, write data modules and launch the web server
356
- if (opts.serve) {
357
- try {
358
- const dataCommitsFile = outputFilePath('data/commits.mjs', outDir)
359
- const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`
360
- writeTextFile(dataCommitsFile, commitsModule)
361
-
362
- const dataCommitsChangedFile = outputFilePath(
363
- 'data/author-changes.mjs',
364
- outDir
365
- )
366
- const jsonText = renderAuthorChangesJson(records)
367
-
368
- const commitsChangedModule = `export default ${JSON.stringify(jsonText, null, 2)};\n`
369
- writeTextFile(dataCommitsChangedFile, commitsChangedModule)
370
-
371
- const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir)
372
- const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n`
373
- writeTextFile(dataStatsFile, statsModule)
374
-
375
- // 新增:每周趋势数据(用于前端图表)
376
- const weekGroups = groupRecords(records, 'week')
377
- const weekKeys = Object.keys(weekGroups).sort()
378
- const weeklySeries = weekKeys.map((k) => {
379
- const s = analyzeOvertime(weekGroups[k], {
380
- startHour:
381
- opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
382
- endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
383
- lunchStart:
384
- opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
385
- lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
386
- country: opts.country || 'CN'
387
- })
388
- return {
389
- period: k,
390
- range: getWeekRange(k),
391
- total: s.total,
392
- outsideWorkCount: s.outsideWorkCount,
393
- outsideWorkRate: s.outsideWorkRate,
394
- nonWorkdayCount: s.nonWorkdayCount,
395
- nonWorkdayRate: s.nonWorkdayRate
396
- }
397
- })
398
- const dataWeeklyFile = outputFilePath(
399
- 'data/overtime-weekly.mjs',
400
- outDir
401
- )
402
- const weeklyModule = `export default ${JSON.stringify(weeklySeries, null, 2)};\n`
403
- writeTextFile(dataWeeklyFile, weeklyModule)
404
- console.log(chalk.green(`Weekly series 已导出: ${dataWeeklyFile}`))
405
-
406
- // 新增:每月趋势数据(用于前端图表)
407
- const monthGroups2 = groupRecords(records, 'month')
408
- const monthKeys2 = Object.keys(monthGroups2).sort()
409
- const monthlySeries = monthKeys2.map((k) => {
410
- const s = analyzeOvertime(monthGroups2[k], {
411
- startHour:
412
- opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
413
- endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
414
- lunchStart:
415
- opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
416
- lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
417
- country: opts.country || 'CN'
418
- })
419
- return {
420
- period: k,
421
- total: s.total,
422
- outsideWorkCount: s.outsideWorkCount,
423
- outsideWorkRate: s.outsideWorkRate,
424
- nonWorkdayCount: s.nonWorkdayCount,
425
- nonWorkdayRate: s.nonWorkdayRate
426
- }
427
- })
428
- const dataMonthlyFile = outputFilePath(
429
- 'data/overtime-monthly.mjs',
430
- outDir
431
- )
432
- const monthlyModule = `export default ${JSON.stringify(monthlySeries, null, 2)};\n`
433
- writeTextFile(dataMonthlyFile, monthlyModule)
434
- console.log(chalk.green(`Monthly series 已导出: ${dataMonthlyFile}`))
435
-
436
- // 新增:每日最晚提交小时(用于显著展示加班严重程度)
437
- const dayGroups2 = groupRecords(records, 'day')
438
- const dayKeys2 = Object.keys(dayGroups2).sort()
439
-
440
- // 次日凌晨归并窗口(默认 6 点前仍算前一天的加班)
441
- const overnightCutoff = Number.isFinite(opts.overnightCutoff)
442
- ? opts.overnightCutoff
443
- : 6
444
- // 次日上班时间(默认按 workStart,若未指定则 9 点)
445
- const workStartHour =
446
- opts.workStart || opts.workStart === 0 ? opts.workStart : 9
447
- const workEndHour =
448
- opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18
449
-
450
- // 有些日期「本身没有 commit」,但第二天凌晨有提交要归并到这一天,
451
- // 需要补出这些“虚拟日期”,否则 latestByDay 会漏掉这天。
452
- const virtualPrevDays = new Set()
453
- records.forEach((r) => {
454
- const d = new Date(r.date)
455
- if (Number.isNaN(d.valueOf())) return
456
- const h = d.getHours()
457
- if (h < 0 || h >= overnightCutoff || h >= workStartHour) return
458
- const curDay = dayjs(d).format('YYYY-MM-DD')
459
- const prevDay = dayjs(curDay).subtract(1, 'day').format('YYYY-MM-DD')
460
- if (!dayGroups2[prevDay]) {
461
- virtualPrevDays.add(prevDay)
462
- }
463
- })
464
-
465
- const allDayKeys = Array.from(
466
- new Set([...dayKeys2, ...virtualPrevDays])
467
- ).sort()
468
-
469
- const latestByDay = allDayKeys.map((k) => {
470
- const list = dayGroups2[k] || []
471
-
472
- // 1) 当天「下班后」的提交:只统计 >= workEndHour 的小时
473
- const sameDayHours = list
474
- .map((r) => new Date(r.date))
475
- .filter((d) => !Number.isNaN(d.valueOf()))
476
- .map((d) => d.getHours())
477
- .filter((h) => h >= workEndHour && h < 24)
478
-
479
- // 2) 次日凌晨、但仍算前一日加班的提交
480
- const nextKey = dayjs(k).add(1, 'day').format('YYYY-MM-DD')
481
- const early = dayGroups2[nextKey] || []
482
- const earlyHours = early
483
- .map((r) => new Date(r.date))
484
- .filter((d) => !Number.isNaN(d.valueOf()))
485
- .map((d) => d.getHours())
486
- // 只看 [0, overnightCutoff) 之间的小时,
487
- // 并且默认认为 < workStartHour 属于「次日上班前」
488
- .filter(
489
- (h) =>
490
- h >= 0 &&
491
- h < overnightCutoff &&
492
- // 保护性判断:若有人把 overnightCutoff 设得大于上班时间,
493
- // 我们仍然只统计到上班时间为止
494
- h < workStartHour
495
- )
496
-
497
- // 3) 计算「逻辑上的最晚加班时间」
498
- // - 当天晚上的用原始小时(如 22 点)
499
- // - 次日凌晨的用 24 + 小时(如 1 点 → 25)
500
- const overtimeValues = [
501
- ...sameDayHours.map((h) => h),
502
- ...earlyHours.map((h) => 24 + h)
503
- ]
504
-
505
- if (overtimeValues.length === 0) {
506
- // 这一天没有任何「下班后到次日上班前」的提交
507
- return {
508
- date: k,
509
- latestHour: null,
510
- latestHourNormalized: null
511
- }
512
- }
513
-
514
- const latestHourNormalized = Math.max(...overtimeValues)
515
-
516
- // latestHour 保留「当天自然日内」的最晚提交通常小时数,供前端需要时参考
517
- const sameDayMax =
518
- sameDayHours.length > 0 ? Math.max(...sameDayHours) : null
519
-
520
- return {
521
- date: k,
522
- latestHour: sameDayMax,
523
- latestHourNormalized
524
- }
525
- })
526
- const dataLatestByDayFile = outputFilePath(
527
- 'data/overtime-latest-by-day.mjs',
528
- outDir
529
- )
530
- const latestByDayModule = `export default ${JSON.stringify(latestByDay, null, 2)};\n`
531
- writeTextFile(dataLatestByDayFile, latestByDayModule)
532
- console.log(
533
- chalk.green(`Latest-by-day series 已导出: ${dataLatestByDayFile}`)
534
- )
535
-
536
- // 导出配置(供前端显示)
537
- try {
538
- const configFile = outputFilePath('data/config.mjs', outDir)
539
- const cfg = {
540
- startHour: opts.workStart || 9,
541
- endHour: opts.workEnd || 18,
542
- lunchStart: opts.lunchStart || 12,
543
- lunchEnd: opts.lunchEnd || 14,
544
- overnightCutoff
545
- }
546
- writeTextFile(
547
- configFile,
548
- `export default ${JSON.stringify(cfg, null, 2)};\n`
549
- )
550
- console.log(chalk.green(`Config 已导出: ${configFile}`))
551
- } catch (e) {
552
- console.warn('Export config failed:', e && e.message ? e.message : e)
553
- }
554
-
555
- startServer(opts.port || 3000, outDir).catch(() => {})
556
- } catch (err) {
557
- console.warn(
558
- 'Export data modules failed:',
559
- err && err.message ? err.message : err
560
- )
561
- }
562
- }
366
+ logDev(`Overtime text 已导出: ${overtimeFile}`)
367
+ logDev(`Overtime table (tabs) 已导出: ${overtimeTabFile}`)
368
+ logDev(`Overtime CSV 已导出: ${overtimeCsvFile}`)
563
369
 
564
370
  // 按月输出 ... 保持原逻辑
565
371
  const perPeriodFormats = String(opts.perPeriodFormats || '')
@@ -578,15 +384,7 @@ const main = async () => {
578
384
  const monthKeys = Object.keys(monthGroups).sort()
579
385
  monthKeys.forEach((k) => {
580
386
  const groupRecs = monthGroups[k]
581
- const s = analyzeOvertime(groupRecs, {
582
- startHour:
583
- opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
584
- endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
585
- lunchStart:
586
- opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
587
- lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
588
- country: opts.country || 'CN'
589
- })
387
+ const s = getOvertimeStats(groupRecs)
590
388
  monthlyContent += `===== ${k} =====\n`
591
389
  monthlyContent += `${renderOvertimeText(s)}\n\n`
592
390
  // Also write a single file per month under 'month/' folder
@@ -594,9 +392,7 @@ const main = async () => {
594
392
  const perMonthFileName = `month/overtime_${outBase}_${k}.txt`
595
393
  const perMonthFile = outputFilePath(perMonthFileName, outDir)
596
394
  writeTextFile(perMonthFile, renderOvertimeText(s))
597
- console.log(
598
- chalk.green(`Overtime 月度(${k}) 已导出: ${perMonthFile}`)
599
- )
395
+ logDev(`Overtime 月度(${k}) 已导出: ${perMonthFile}`)
600
396
  // per-period CSV / Tab format (按需生成)
601
397
  if (perPeriodFormats.includes('csv')) {
602
398
  try {
@@ -605,10 +401,8 @@ const main = async () => {
605
401
  outputFilePath(perMonthCsvName, outDir),
606
402
  renderOvertimeCsv(s)
607
403
  )
608
- console.log(
609
- chalk.green(
610
- `Overtime 月度(CSV)(${k}) 已导出: ${outputFilePath(perMonthCsvName, outDir)}`
611
- )
404
+ logDev(
405
+ `Overtime 月度(CSV)(${k}) 已导出: ${outputFilePath(perMonthCsvName, outDir)}`
612
406
  )
613
407
  } catch (err) {
614
408
  console.warn(
@@ -624,10 +418,8 @@ const main = async () => {
624
418
  outputFilePath(perMonthTabName, outDir),
625
419
  renderOvertimeTab(s)
626
420
  )
627
- console.log(
628
- chalk.green(
629
- `Overtime 月度(Tab)(${k}) 已导出: ${outputFilePath(perMonthTabName, outDir)}`
630
- )
421
+ logDev(
422
+ `Overtime 月度(Tab)(${k}) 已导出: ${outputFilePath(perMonthTabName, outDir)}`
631
423
  )
632
424
  } catch (err) {
633
425
  console.warn(
@@ -645,7 +437,7 @@ const main = async () => {
645
437
  })
646
438
  if (!opts.perPeriodOnly) {
647
439
  writeTextFile(monthlyFile, monthlyContent)
648
- console.log(chalk.green(`Overtime 月度汇总 已导出: ${monthlyFile}`))
440
+ logDev(`Overtime 月度汇总 已导出: ${monthlyFile}`)
649
441
  }
650
442
  // per-period Excel (sheets or files)
651
443
  if (perPeriodFormats.includes('xlsx')) {
@@ -658,9 +450,7 @@ const main = async () => {
658
450
  stats: opts.stats,
659
451
  gerrit: opts.gerrit
660
452
  })
661
- console.log(
662
- chalk.green(`Overtime 月度(XLSX) 已导出: ${monthXlsxFile}`)
663
- )
453
+ logDev(`Overtime 月度(XLSX) 已导出: ${monthXlsxFile}`)
664
454
  } catch (err) {
665
455
  console.warn(
666
456
  'Export month XLSX (sheets) failed:',
@@ -710,22 +500,16 @@ const main = async () => {
710
500
  const weekKeys = Object.keys(weekGroups).sort()
711
501
  weekKeys.forEach((k) => {
712
502
  const groupRecs = weekGroups[k]
713
- const s = analyzeOvertime(groupRecs, {
714
- startHour:
715
- opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
716
- endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
717
- lunchStart:
718
- opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
719
- lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
720
- country: opts.country || 'CN'
721
- })
503
+ const s = getOvertimeStats(groupRecs)
722
504
  weeklyContent += `===== ${k} =====\n`
723
505
  weeklyContent += `${renderOvertimeText(s)}\n\n`
724
506
  try {
725
507
  const perWeekFileName = `week/overtime_${outBase}_${k}.txt`
726
508
  const perWeekFile = outputFilePath(perWeekFileName, outDir)
727
509
  writeTextFile(perWeekFile, renderOvertimeText(s))
728
- console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`))
510
+ // console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`))
511
+ logDev(`Overtime 周度(${k}) 已导出: ${perWeekFile}`)
512
+
729
513
  // eslint-disable-next-line no-shadow
730
514
  const perPeriodFormats = String(opts.perPeriodFormats || '')
731
515
  .split(',')
@@ -743,10 +527,8 @@ const main = async () => {
743
527
  outputFilePath(perWeekCsvName, outDir),
744
528
  renderOvertimeCsv(s)
745
529
  )
746
- console.log(
747
- chalk.green(
748
- `Overtime 周度(CSV)(${k}) 已导出: ${outputFilePath(perWeekCsvName, outDir)}`
749
- )
530
+ logDev(
531
+ `Overtime 周度(CSV)(${k}) 已导出: ${outputFilePath(perWeekCsvName, outDir)}`
750
532
  )
751
533
  } catch (err) {
752
534
  console.warn(
@@ -762,10 +544,8 @@ const main = async () => {
762
544
  outputFilePath(perWeekTabName, outDir),
763
545
  renderOvertimeTab(s)
764
546
  )
765
- console.log(
766
- chalk.green(
767
- `Overtime 周度(Tab)(${k}) 已导出: ${outputFilePath(perWeekTabName, outDir)}`
768
- )
547
+ logDev(
548
+ `Overtime 周度(Tab)(${k}) 已导出: ${outputFilePath(perWeekTabName, outDir)}`
769
549
  )
770
550
  } catch (err) {
771
551
  console.warn(
@@ -782,7 +562,7 @@ const main = async () => {
782
562
  }
783
563
  })
784
564
  writeTextFile(weeklyFile, weeklyContent)
785
- console.log(chalk.green(`Overtime 周度汇总 已导出: ${weeklyFile}`))
565
+ logDev(`Overtime 周度汇总 已导出: ${weeklyFile}`)
786
566
  } catch (err) {
787
567
  console.warn(
788
568
  'Generate weekly overtime failed:',
@@ -798,8 +578,8 @@ const main = async () => {
798
578
  writeJSON(filepath, groups || records)
799
579
  const jsonText = renderAuthorChangesJson(records)
800
580
  writeJSON(outputFilePath('author-changes.json', outDir), jsonText)
801
- console.log(chalk.green(`JSON 已导出: ${filepath}`))
802
- spinner.succeed('Done')
581
+ logDev(`JSON 已导出: ${filepath}`)
582
+ handleSuccess({ startTime, spinner })
803
583
  return
804
584
  }
805
585
 
@@ -809,13 +589,16 @@ const main = async () => {
809
589
  const text = renderText(records, groups, { showGerrit: !!opts.gerrit })
810
590
  writeTextFile(filepath, text)
811
591
  writeTextFile(
812
- outputFilePath('author-changes.text', outDir),
592
+ outputFilePath('author-changes.txt', outDir),
813
593
  renderChangedLinesText(records)
814
594
  )
815
595
 
816
- console.log(text)
817
- console.log(chalk.green(`文本已导出: ${filepath}`))
818
- spinner.succeed('Done')
596
+ console.log('\n Commits List:\n', text, '\n')
597
+
598
+ logDev(`文本已导出: ${filepath}`)
599
+
600
+ handleSuccess({ startTime, spinner })
601
+
819
602
  return
820
603
  }
821
604
 
@@ -835,9 +618,10 @@ const main = async () => {
835
618
  )
836
619
  const text = renderText(records, groups)
837
620
  writeTextFile(txtPath, text)
838
- console.log(chalk.green(`Excel 已导出: ${excelPath}`))
839
- console.log(chalk.green(`文本已自动导出: ${txtPath}`))
840
- spinner.succeed('Done')
621
+ logDev(`Excel 已导出: ${excelPath}`)
622
+ logDev(`文本已自动导出: ${txtPath}`)
623
+
624
+ handleSuccess({ startTime, spinner })
841
625
  }
842
626
 
843
627
  await autoCheckUpdate()
@@ -0,0 +1,11 @@
1
+ const store = {
2
+ debug: false,
3
+ }
4
+
5
+ export function setConfig(key, value) {
6
+ store[key] = value
7
+ }
8
+
9
+ export function getConfig(key) {
10
+ return store[key]
11
+ }
@@ -0,0 +1,14 @@
1
+ // memoize.mjs
2
+ export function memoize(fn) {
3
+ const cache = new Map()
4
+
5
+ return function (...args) {
6
+ const key = JSON.stringify(args)
7
+ if (cache.has(key)) {
8
+ return cache.get(key)
9
+ }
10
+ const result = fn(...args)
11
+ cache.set(key, result)
12
+ return result
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ import { analyzeOvertime } from './overtime.mjs';
2
+
3
+ export function createOvertimeStats(defaultConfig = {}) {
4
+ return function getOvertimeStats(records, overrides) {
5
+ const config = overrides
6
+ ? { ...defaultConfig, ...overrides }
7
+ : defaultConfig;
8
+
9
+ return analyzeOvertime(records, config);
10
+ };
11
+ }
@@ -0,0 +1,7 @@
1
+ // analyzeOvertimeCached.mjs
2
+ import { memoize } from '../lib/memoize.mjs'
3
+ import {
4
+ analyzeOvertime
5
+ } from './overtime.mjs'
6
+
7
+ export const analyzeOvertimeCached = memoize(analyzeOvertime)
@@ -1,26 +1,20 @@
1
- // authorNormalizer.js
2
1
  /**
3
2
  * 按邮箱自动归一化作者名字
4
- * - 优先使用中文姓名
5
- * - 如果同邮箱出现多个非中文名,保持第一个
6
- * - 自动清理空格与不可见字符
3
+ * - 中文名优先覆盖
4
+ * - 同邮箱多个英文名保持第一个
7
5
  * - 保留原始 author 以便 debug
8
6
  */
9
7
  export function createAuthorNormalizer() {
10
8
  const map = {} // email -> canonical name
11
- const originalMap = {} // email -> 原始所有 author
9
+ const originalMap = {} // email -> Set of original author names
12
10
 
13
11
  function isChinese(str) {
14
12
  return /[\u4e00-\u9fa5]/.test(str)
15
13
  }
16
14
 
17
15
  function cleanName(str) {
18
- if (!str) return ''
19
- return str
20
- // eslint-disable-next-line no-misleading-character-class
21
- .replace(/[\u200B\u200C\u200D\uFEFF]/g, '') // 去零宽空格
22
- .replace(/\s+/g, ' ') // 多空格 -> 单空格
23
- .trim()
16
+ // eslint-disable-next-line no-misleading-character-class
17
+ return (str || '').replace(/[\u200B\u200C\u200D\uFEFF]/g, '').trim()
24
18
  }
25
19
 
26
20
  function cleanEmail(str) {
@@ -28,31 +22,30 @@ export function createAuthorNormalizer() {
28
22
  }
29
23
 
30
24
  function getAuthor(name, email) {
31
- const cleanedName = cleanName(name)
32
- const cleanedEmail = cleanEmail(email)
25
+ const n = cleanName(name)
26
+ const e = cleanEmail(email)
33
27
 
34
- if (!cleanedEmail) return cleanedName || 'Unknown'
28
+ if (!e) return n || 'Unknown'
35
29
 
36
- // 记录原始 author 便于 debug
37
- if (!originalMap[cleanedEmail]) originalMap[cleanedEmail] = new Set()
38
- if (cleanedName) originalMap[cleanedEmail].add(cleanedName)
30
+ // 记录原始 author
31
+ if (!originalMap[e]) originalMap[e] = new Set()
32
+ if (n) originalMap[e].add(n)
39
33
 
40
- const canonical = map[cleanedEmail]
34
+ const prev = map[e]
41
35
 
42
- // 首次遇到这个邮箱 → 记录当前作者名
43
- if (!canonical) {
44
- map[cleanedEmail] = cleanedName || cleanedEmail
45
- return map[cleanedEmail]
36
+ // 中文名优先覆盖
37
+ if (isChinese(n)) {
38
+ map[e] = n
39
+ return n
46
40
  }
47
41
 
48
- // 新名是中文覆盖旧的
49
- if (isChinese(cleanedName)) {
50
- map[cleanedEmail] = cleanedName
51
- return map[cleanedEmail]
52
- }
42
+ // 已有中文名返回中文
43
+ if (prev && isChinese(prev)) return prev
44
+
45
+ // 首次出现英文名 → 记录
46
+ if (!prev) map[e] = n
53
47
 
54
- // 新名非中文 → 保留已有中文或旧名
55
- return canonical
48
+ return map[e]
56
49
  }
57
50
 
58
51
  return {