wukong-gitlog-cli 1.0.36 → 1.0.38
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/.eslintrc +1 -0
- package/CHANGELOG.md +21 -0
- package/bin/wukong-gitlog-cli +1 -1
- package/package.json +19 -14
- package/src/cli/parseOptions.mjs +9 -0
- package/src/git.mjs +23 -17
- package/src/handlers/handleServe.mjs +203 -0
- package/src/{cli.mjs → index.mjs} +108 -276
- package/src/lib/configStore.mjs +11 -0
- package/src/lib/memoize.mjs +14 -0
- package/src/overtime/createOvertimeStats.mjs +11 -0
- package/src/utils/analyzeOvertimeCached.mjs +7 -0
- package/src/utils/emoji.mjs +83 -0
- package/src/utils/exitWithTime.mjs +17 -0
- package/src/utils/getWeekRange.mjs +16 -0
- package/src/utils/handleSuccess.mjs +9 -0
- package/src/utils/logDev.mjs +19 -0
- package/src/utils/logger.mjs +145 -0
- package/src/utils/profiler/diff.mjs +26 -0
- package/src/utils/profiler/format.mjs +11 -0
- package/src/utils/profiler/index.mjs +144 -0
- package/src/utils/profiler/trace.mjs +26 -0
- package/src/utils/profiler.mjs +101 -0
- package/src/utils/time/scopeTimer.mjs +37 -0
- package/src/utils/time/timer.mjs +33 -0
- package/src/utils/time/withTimer.mjs +11 -0
- package/src/utils/timer.mjs +35 -0
- /package/src/{overtime.mjs → overtime/overtime.mjs} +0 -0
package/.eslintrc
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.0.38](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.37...v1.0.38) (2025-12-27)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 🎸 增加性能分析 ([ec925cc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/ec925cc3d20be2a0eee6e20aaee99b4449eff9be))
|
|
11
|
+
* 🎸 flame 和 显示 flame-like 日志 ([7b1a00c](https://github.com/tomatobybike/wukong-gitlog-cli/commit/7b1a00c26afa1757637e6c4a3a2be92d74e0ad65))
|
|
12
|
+
* 🎸 ignore ([eb3e5bc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/eb3e5bc89183e0722056036f14f74806e3c3b1e3))
|
|
13
|
+
* 🎸 profiler ([48fd0a5](https://github.com/tomatobybike/wukong-gitlog-cli/commit/48fd0a5a8b25b55bbca7bbdbd2df11158049adcd))
|
|
14
|
+
* 🎸 Profiler ([a7f4623](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a7f4623a162947d31f25702052f54a4ff0406fcb))
|
|
15
|
+
|
|
16
|
+
### [1.0.37](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.36...v1.0.37) (2025-12-12)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* 🎸 add logDev ([a85905b](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a85905b038014fb62c7fd21713f63e2405e3cfa7))
|
|
22
|
+
* 🎸 createOvertimeStats ([abb0051](https://github.com/tomatobybike/wukong-gitlog-cli/commit/abb0051efc91cceb9845a1c5c9a00f9d6747baf4))
|
|
23
|
+
* 🎸 logDev ([e122042](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e12204203a077630784caa16b01c566fdd72b84a))
|
|
24
|
+
* 🎸 logger debug ([32a55fe](https://github.com/tomatobybike/wukong-gitlog-cli/commit/32a55fe8d191969ca887cb46789d14b65b446f6d))
|
|
25
|
+
|
|
5
26
|
### [1.0.36](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.35...v1.0.36) (2025-12-12)
|
|
6
27
|
|
|
7
28
|
|
package/bin/wukong-gitlog-cli
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import('../src/
|
|
2
|
+
import('../src/index.mjs')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wukong-gitlog-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.38",
|
|
4
4
|
"description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -37,40 +37,42 @@
|
|
|
37
37
|
"wukong-gitlog-cli": "./bin/wukong-gitlog-cli"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
+
"build": "node scripts/esbuild.config.mjs",
|
|
41
|
+
"check:npm:login": "node scripts/check-npm-login.mjs",
|
|
40
42
|
"cli:excel-demo": "node ./src/cli.mjs --overtime --format excel --stats --limit 5 --out commits.xlsx",
|
|
41
43
|
"cli:excel-demo-parent": "node ./src/cli.mjs --out-parent --format excel --stats --limit 5 --out commits-parent.xlsx",
|
|
42
44
|
"cli:gerrit-changeid-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{changeId}}\" --limit 5 --out commits-gerrit-changeid.txt",
|
|
43
45
|
"cli:gerrit-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{hash}}\" --limit 5 --out commits-gerrit.txt",
|
|
44
46
|
"cli:help": "node ./src/cli.mjs --help",
|
|
45
47
|
"cli:json-demo": "node ./src/cli.mjs --json --limit 5 --out commits.json",
|
|
46
|
-
"cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
|
|
47
|
-
"cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
|
|
48
48
|
"cli:overtime-demo": "node ./src/cli.mjs --overtime --format text --limit 200",
|
|
49
|
-
"cli:overtime-text": "node ./src/cli.mjs --overtime --limit 100 --format text --out commits.txt",
|
|
50
49
|
"cli:overtime-excel": "node ./src/cli.mjs --overtime --limit 50 --format excel --out commits.xlsx",
|
|
51
|
-
"cli:overtime-text-us": "node ./src/cli.mjs --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
52
|
-
"cli:overtime-text-us-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
53
|
-
"cli:overtime-text-us-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
54
50
|
"cli:overtime-excel-cn": "node ./src/cli.mjs --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
55
|
-
"cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
56
51
|
"cli:overtime-excel-cn-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
52
|
+
"cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
57
53
|
"cli:overtime-per-period-csv-tab": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab",
|
|
58
|
-
"cli:overtime-per-period-xlsx-sheets": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets",
|
|
59
|
-
"cli:overtime-per-period-xlsx-files": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files",
|
|
60
54
|
"cli:overtime-per-period-only": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-only",
|
|
55
|
+
"cli:overtime-per-period-xlsx-files": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files",
|
|
56
|
+
"cli:overtime-per-period-xlsx-sheets": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets",
|
|
57
|
+
"cli:overtime-text": "node ./src/cli.mjs --overtime --limit 100 --format text --out commits.txt",
|
|
58
|
+
"cli:overtime-text-us": "node ./src/cli.mjs --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
59
|
+
"cli:overtime-text-us-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
60
|
+
"cli:overtime-text-us-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
|
|
61
61
|
"cli:serve": "node ./src/cli.mjs --serve --overtime --format text --out commits.txt",
|
|
62
62
|
"cli:serve-only": "node ./src/cli.mjs --serve-only",
|
|
63
|
+
"cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
|
|
64
|
+
"cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
|
|
63
65
|
"format": "prettier --write \"src/**/*.{js,mjs}\"",
|
|
64
66
|
"lint": "eslint src --ext .js,.mjs src",
|
|
65
67
|
"lint:fix": "eslint src --ext .js,.mjs --fix",
|
|
66
68
|
"pack": "npm pack",
|
|
67
|
-
"build": "node scripts/esbuild.config.mjs",
|
|
68
69
|
"prepare": "husky install",
|
|
69
|
-
"prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
|
|
70
70
|
"prepublishOnly": "yarn lint && yarn build",
|
|
71
|
-
"
|
|
72
|
-
"release": "yarn check:npm:login && yarn release:patch && yarn push:tags && npm publish",
|
|
71
|
+
"prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
|
|
73
72
|
"push:tags": "git push origin main --follow-tags",
|
|
73
|
+
"prerelease": "yarn lint",
|
|
74
|
+
"release": "yarn check:npm:login && yarn release:patch && npm publish && yarn push:tags",
|
|
75
|
+
"release:rollback": "git reset --hard HEAD~1 && git tag -d $(git tag --points-at HEAD~1)",
|
|
74
76
|
"release:major": "yarn lint && standard-version --release-as major",
|
|
75
77
|
"release:minor": "yarn lint && standard-version --release-as minor",
|
|
76
78
|
"release:patch": "yarn lint && standard-version --release-as patch",
|
|
@@ -94,13 +96,16 @@
|
|
|
94
96
|
"boxen": "8.0.1",
|
|
95
97
|
"chalk": "5.6.2",
|
|
96
98
|
"commander": "12.1.0",
|
|
99
|
+
"date-fns": "4.1.0",
|
|
97
100
|
"date-holidays": "2.1.1",
|
|
98
101
|
"dayjs": "1.11.19",
|
|
102
|
+
"dotenv": "17.2.2",
|
|
99
103
|
"exceljs": "4.4.0",
|
|
100
104
|
"is-online": "12.0.2",
|
|
101
105
|
"ora": "9.0.0",
|
|
102
106
|
"semver": "6.3.1",
|
|
103
107
|
"string-width": "5.1.2",
|
|
108
|
+
"wukong-profiler": "^1.0.5",
|
|
104
109
|
"zx": "7.2.4"
|
|
105
110
|
},
|
|
106
111
|
"devDependencies": {
|
package/src/git.mjs
CHANGED
|
@@ -76,21 +76,20 @@ export async function getGitLogsSlow(opts) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
return commits
|
|
79
|
+
return { commits, authorMap: {} }
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export async function getGitLogsQuick(opts) {
|
|
83
83
|
const { author, email, since, until, limit, merges } = opts
|
|
84
84
|
|
|
85
|
-
const pretty =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
].join('%x1f') }%x1e`
|
|
85
|
+
const pretty = `${[
|
|
86
|
+
'%H', // hash
|
|
87
|
+
'%an', // author name
|
|
88
|
+
'%ae', // email
|
|
89
|
+
'%ad', // date
|
|
90
|
+
'%s', // subject
|
|
91
|
+
'%B' // body
|
|
92
|
+
].join('%x1f')}%x1e`
|
|
94
93
|
|
|
95
94
|
const args = [
|
|
96
95
|
'log',
|
|
@@ -177,11 +176,16 @@ export async function getGitLogsFast(opts = {}) {
|
|
|
177
176
|
'%an', // author name
|
|
178
177
|
'%ae', // email
|
|
179
178
|
'%ad', // date
|
|
180
|
-
'%s',
|
|
181
|
-
'%B'
|
|
182
|
-
].join('%x1f')
|
|
179
|
+
'%s', // subject
|
|
180
|
+
'%B' // body
|
|
181
|
+
].join('%x1f')}%x1e`
|
|
183
182
|
|
|
184
|
-
const args = [
|
|
183
|
+
const args = [
|
|
184
|
+
'log',
|
|
185
|
+
`--pretty=format:${pretty}`,
|
|
186
|
+
'--date=iso-local',
|
|
187
|
+
'--numstat'
|
|
188
|
+
]
|
|
185
189
|
|
|
186
190
|
if (author) args.push(`--author=${author}`)
|
|
187
191
|
if (email) args.push(`--author=${email}`)
|
|
@@ -196,13 +200,15 @@ export async function getGitLogsFast(opts = {}) {
|
|
|
196
200
|
const commits = []
|
|
197
201
|
|
|
198
202
|
// 匹配每个 commit header + numstat
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
const commitRegex =
|
|
204
|
+
// eslint-disable-next-line no-control-regex
|
|
205
|
+
/([0-9a-f]+)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([\s\S]*?)(?=(?:[0-9a-f]{7,40}\x1f)|\x1e$)/g
|
|
201
206
|
let match
|
|
202
207
|
|
|
203
208
|
for (const ns of raw.matchAll(commitRegex)) {
|
|
204
209
|
const [_, hash, authorName, emailAddr, date, subject, bodyAndNumstat] = ns
|
|
205
|
-
const [, changeId] =
|
|
210
|
+
const [, changeId] =
|
|
211
|
+
bodyAndNumstat.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
|
|
206
212
|
|
|
207
213
|
const c = {
|
|
208
214
|
hash,
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line no-unused-vars
|
|
4
|
+
import { renderAuthorChangesJson } from '../json.mjs'
|
|
5
|
+
import { startServer } from '../server.mjs'
|
|
6
|
+
import { getWeekRange } from '../utils/getWeekRange.mjs'
|
|
7
|
+
import { groupRecords, outputFilePath, writeTextFile } from '../utils/index.mjs'
|
|
8
|
+
import { logDev } from '../utils/logDev.mjs'
|
|
9
|
+
|
|
10
|
+
export const handleServe = async ({
|
|
11
|
+
opts,
|
|
12
|
+
outDir,
|
|
13
|
+
records,
|
|
14
|
+
getOvertimeStats
|
|
15
|
+
}) => {
|
|
16
|
+
try {
|
|
17
|
+
const stats = getOvertimeStats(records)
|
|
18
|
+
|
|
19
|
+
const dataCommitsFile = outputFilePath('data/commits.mjs', outDir)
|
|
20
|
+
const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`
|
|
21
|
+
writeTextFile(dataCommitsFile, commitsModule)
|
|
22
|
+
|
|
23
|
+
const dataCommitsChangedFile = outputFilePath(
|
|
24
|
+
'data/author-changes.mjs',
|
|
25
|
+
outDir
|
|
26
|
+
)
|
|
27
|
+
const jsonText = renderAuthorChangesJson(records)
|
|
28
|
+
|
|
29
|
+
const commitsChangedModule = `export default ${JSON.stringify(jsonText, null, 2)};\n`
|
|
30
|
+
writeTextFile(dataCommitsChangedFile, commitsChangedModule)
|
|
31
|
+
|
|
32
|
+
const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir)
|
|
33
|
+
const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n`
|
|
34
|
+
writeTextFile(dataStatsFile, statsModule)
|
|
35
|
+
|
|
36
|
+
// 新增:每周趋势数据(用于前端图表)
|
|
37
|
+
const weekGroups = groupRecords(records, 'week')
|
|
38
|
+
const weekKeys = Object.keys(weekGroups).sort()
|
|
39
|
+
const weeklySeries = weekKeys.map((k) => {
|
|
40
|
+
const s = getOvertimeStats(weekGroups[k])
|
|
41
|
+
return {
|
|
42
|
+
period: k,
|
|
43
|
+
range: getWeekRange(k),
|
|
44
|
+
total: s.total,
|
|
45
|
+
outsideWorkCount: s.outsideWorkCount,
|
|
46
|
+
outsideWorkRate: s.outsideWorkRate,
|
|
47
|
+
nonWorkdayCount: s.nonWorkdayCount,
|
|
48
|
+
nonWorkdayRate: s.nonWorkdayRate
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
const dataWeeklyFile = outputFilePath('data/overtime-weekly.mjs', outDir)
|
|
52
|
+
const weeklyModule = `export default ${JSON.stringify(weeklySeries, null, 2)};\n`
|
|
53
|
+
writeTextFile(dataWeeklyFile, weeklyModule)
|
|
54
|
+
logDev(`Weekly series 已导出: ${dataWeeklyFile}`)
|
|
55
|
+
|
|
56
|
+
// 新增:每月趋势数据(用于前端图表)
|
|
57
|
+
const monthGroups2 = groupRecords(records, 'month')
|
|
58
|
+
const monthKeys2 = Object.keys(monthGroups2).sort()
|
|
59
|
+
const monthlySeries = monthKeys2.map((k) => {
|
|
60
|
+
const s = getOvertimeStats(monthGroups2[k])
|
|
61
|
+
return {
|
|
62
|
+
period: k,
|
|
63
|
+
total: s.total,
|
|
64
|
+
outsideWorkCount: s.outsideWorkCount,
|
|
65
|
+
outsideWorkRate: s.outsideWorkRate,
|
|
66
|
+
nonWorkdayCount: s.nonWorkdayCount,
|
|
67
|
+
nonWorkdayRate: s.nonWorkdayRate
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
const dataMonthlyFile = outputFilePath('data/overtime-monthly.mjs', outDir)
|
|
71
|
+
const monthlyModule = `export default ${JSON.stringify(monthlySeries, null, 2)};\n`
|
|
72
|
+
writeTextFile(dataMonthlyFile, monthlyModule)
|
|
73
|
+
logDev(`Monthly series 已导出: ${dataMonthlyFile}`)
|
|
74
|
+
|
|
75
|
+
// 新增:每日最晚提交小时(用于显著展示加班严重程度)
|
|
76
|
+
const dayGroups2 = groupRecords(records, 'day')
|
|
77
|
+
const dayKeys2 = Object.keys(dayGroups2).sort()
|
|
78
|
+
|
|
79
|
+
// 次日凌晨归并窗口(默认 6 点前仍算前一天的加班)
|
|
80
|
+
const overnightCutoff = Number.isFinite(opts.overnightCutoff)
|
|
81
|
+
? opts.overnightCutoff
|
|
82
|
+
: 6
|
|
83
|
+
// 次日上班时间(默认按 workStart,若未指定则 9 点)
|
|
84
|
+
const workStartHour =
|
|
85
|
+
opts.workStart || opts.workStart === 0 ? opts.workStart : 9
|
|
86
|
+
const workEndHour = opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18
|
|
87
|
+
|
|
88
|
+
// 有些日期「本身没有 commit」,但第二天凌晨有提交要归并到这一天,
|
|
89
|
+
// 需要补出这些“虚拟日期”,否则 latestByDay 会漏掉这天。
|
|
90
|
+
const virtualPrevDays = new Set()
|
|
91
|
+
records.forEach((r) => {
|
|
92
|
+
const d = new Date(r.date)
|
|
93
|
+
if (Number.isNaN(d.valueOf())) return
|
|
94
|
+
const h = d.getHours()
|
|
95
|
+
if (h < 0 || h >= overnightCutoff || h >= workStartHour) return
|
|
96
|
+
const curDay = dayjs(d).format('YYYY-MM-DD')
|
|
97
|
+
const prevDay = dayjs(curDay).subtract(1, 'day').format('YYYY-MM-DD')
|
|
98
|
+
if (!dayGroups2[prevDay]) {
|
|
99
|
+
virtualPrevDays.add(prevDay)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const allDayKeys = Array.from(
|
|
104
|
+
new Set([...dayKeys2, ...virtualPrevDays])
|
|
105
|
+
).sort()
|
|
106
|
+
|
|
107
|
+
const latestByDay = allDayKeys.map((k) => {
|
|
108
|
+
const list = dayGroups2[k] || []
|
|
109
|
+
|
|
110
|
+
// 1) 当天「下班后」的提交:只统计 >= workEndHour 的小时
|
|
111
|
+
const sameDayHours = list
|
|
112
|
+
.map((r) => new Date(r.date))
|
|
113
|
+
.filter((d) => !Number.isNaN(d.valueOf()))
|
|
114
|
+
.map((d) => d.getHours())
|
|
115
|
+
.filter((h) => h >= workEndHour && h < 24)
|
|
116
|
+
|
|
117
|
+
// 2) 次日凌晨、但仍算前一日加班的提交
|
|
118
|
+
const nextKey = dayjs(k).add(1, 'day').format('YYYY-MM-DD')
|
|
119
|
+
const early = dayGroups2[nextKey] || []
|
|
120
|
+
const earlyHours = early
|
|
121
|
+
.map((r) => new Date(r.date))
|
|
122
|
+
.filter((d) => !Number.isNaN(d.valueOf()))
|
|
123
|
+
.map((d) => d.getHours())
|
|
124
|
+
// 只看 [0, overnightCutoff) 之间的小时,
|
|
125
|
+
// 并且默认认为 < workStartHour 属于「次日上班前」
|
|
126
|
+
.filter(
|
|
127
|
+
(h) =>
|
|
128
|
+
h >= 0 &&
|
|
129
|
+
h < overnightCutoff &&
|
|
130
|
+
// 保护性判断:若有人把 overnightCutoff 设得大于上班时间,
|
|
131
|
+
// 我们仍然只统计到上班时间为止
|
|
132
|
+
h < workStartHour
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// 3) 计算「逻辑上的最晚加班时间」
|
|
136
|
+
// - 当天晚上的用原始小时(如 22 点)
|
|
137
|
+
// - 次日凌晨的用 24 + 小时(如 1 点 → 25)
|
|
138
|
+
const overtimeValues = [
|
|
139
|
+
...sameDayHours.map((h) => h),
|
|
140
|
+
...earlyHours.map((h) => 24 + h)
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
if (overtimeValues.length === 0) {
|
|
144
|
+
// 这一天没有任何「下班后到次日上班前」的提交
|
|
145
|
+
return {
|
|
146
|
+
date: k,
|
|
147
|
+
latestHour: null,
|
|
148
|
+
latestHourNormalized: null
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const latestHourNormalized = Math.max(...overtimeValues)
|
|
153
|
+
|
|
154
|
+
// latestHour 保留「当天自然日内」的最晚提交通常小时数,供前端需要时参考
|
|
155
|
+
const sameDayMax =
|
|
156
|
+
sameDayHours.length > 0 ? Math.max(...sameDayHours) : null
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
date: k,
|
|
160
|
+
latestHour: sameDayMax,
|
|
161
|
+
latestHourNormalized
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
const dataLatestByDayFile = outputFilePath(
|
|
165
|
+
'data/overtime-latest-by-day.mjs',
|
|
166
|
+
outDir
|
|
167
|
+
)
|
|
168
|
+
const latestByDayModule = `export default ${JSON.stringify(latestByDay, null, 2)};\n`
|
|
169
|
+
writeTextFile(dataLatestByDayFile, latestByDayModule)
|
|
170
|
+
logDev(`Latest-by-day series 已导出: ${dataLatestByDayFile}`)
|
|
171
|
+
|
|
172
|
+
// 导出配置(供前端显示)
|
|
173
|
+
try {
|
|
174
|
+
const configFile = outputFilePath('data/config.mjs', outDir)
|
|
175
|
+
const cfg = {
|
|
176
|
+
startHour: opts.workStart || 9,
|
|
177
|
+
endHour: opts.workEnd || 18,
|
|
178
|
+
lunchStart: opts.lunchStart || 12,
|
|
179
|
+
lunchEnd: opts.lunchEnd || 14,
|
|
180
|
+
overnightCutoff
|
|
181
|
+
}
|
|
182
|
+
writeTextFile(
|
|
183
|
+
configFile,
|
|
184
|
+
`export default ${JSON.stringify(cfg, null, 2)};\n`
|
|
185
|
+
)
|
|
186
|
+
logDev(`Config 已导出: ${configFile}`)
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.warn('Export config failed:', e && e.message ? e.message : e)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
startServer(opts.port || 3000, outDir).catch((err) => {
|
|
192
|
+
console.warn(
|
|
193
|
+
'Start server failed.',
|
|
194
|
+
err && err.message ? err.message : err
|
|
195
|
+
)
|
|
196
|
+
})
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.warn(
|
|
199
|
+
'Export data modules failed:',
|
|
200
|
+
err && err.message ? err.message : err
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|