wukong-gitlog-cli 1.0.18 → 1.0.19
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 +8 -0
- package/package.json +1 -1
- package/web/app.js +306 -0
- package/web/index.html +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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.19](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.18...v1.0.19) (2025-12-05)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 🎸 latestMonthlyRiskSummary ([e8a442e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e8a442e8333ac303815e7eaaf3aa571e7e976bd6))
|
|
11
|
+
* 🎸 monthlyDurationRiskSummary ([d853876](https://github.com/tomatobybike/wukong-gitlog-cli/commit/d853876a5457bac57fbe4a000be90308b49d8b9d))
|
|
12
|
+
|
|
5
13
|
### [1.0.18](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.17...v1.0.18) (2025-12-05)
|
|
6
14
|
|
|
7
15
|
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -1480,6 +1480,9 @@ function drawAuthorOvertimeTrends(commits, stats) {
|
|
|
1480
1480
|
|
|
1481
1481
|
// 输出本周风险总结
|
|
1482
1482
|
renderWeeklyRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1483
|
+
renderMonthlyRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1484
|
+
renderWeeklyDurationRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1485
|
+
renderMonthlyDurationRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1483
1486
|
|
|
1484
1487
|
return chart
|
|
1485
1488
|
}
|
|
@@ -1579,6 +1582,225 @@ function renderWeeklyRiskSummary(
|
|
|
1579
1582
|
`
|
|
1580
1583
|
}
|
|
1581
1584
|
|
|
1585
|
+
function computeAuthorDailyMaxOvertime(commits, startHour, endHour, cutoff) {
|
|
1586
|
+
const byAuthorDay = new Map()
|
|
1587
|
+
commits.forEach((c) => {
|
|
1588
|
+
const d = new Date(c.date)
|
|
1589
|
+
if (Number.isNaN(d.valueOf())) return
|
|
1590
|
+
const h = d.getHours()
|
|
1591
|
+
let overtime = null
|
|
1592
|
+
let dayKey = null
|
|
1593
|
+
if (h >= endHour && h < 24) {
|
|
1594
|
+
overtime = h - endHour
|
|
1595
|
+
dayKey = d.toISOString().slice(0, 10)
|
|
1596
|
+
} else if (h >= 0 && h < cutoff && h < startHour) {
|
|
1597
|
+
overtime = 24 - endHour + h
|
|
1598
|
+
const cur = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()))
|
|
1599
|
+
cur.setUTCDate(cur.getUTCDate() - 1)
|
|
1600
|
+
dayKey = cur.toISOString().slice(0, 10)
|
|
1601
|
+
}
|
|
1602
|
+
if (overtime == null || !dayKey) return
|
|
1603
|
+
const author = c.author || 'unknown'
|
|
1604
|
+
if (!byAuthorDay.has(author)) byAuthorDay.set(author, new Map())
|
|
1605
|
+
const m = byAuthorDay.get(author)
|
|
1606
|
+
const cur = m.get(dayKey)
|
|
1607
|
+
if (!cur || overtime > cur) m.set(dayKey, overtime)
|
|
1608
|
+
})
|
|
1609
|
+
return byAuthorDay
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function renderWeeklyDurationRiskSummary(
|
|
1613
|
+
commits,
|
|
1614
|
+
{ startHour = 9, endHour = 18, cutoff = 6 } = {}
|
|
1615
|
+
) {
|
|
1616
|
+
const box = document.getElementById('weeklyDurationRiskSummary')
|
|
1617
|
+
if (!box) return
|
|
1618
|
+
const now = new Date()
|
|
1619
|
+
const curWeek = getIsoWeekKey(now.toISOString().slice(0, 10))
|
|
1620
|
+
const byAuthorDay = computeAuthorDailyMaxOvertime(commits, startHour, endHour, cutoff)
|
|
1621
|
+
const sums = []
|
|
1622
|
+
byAuthorDay.forEach((dayMap, author) => {
|
|
1623
|
+
let total = 0
|
|
1624
|
+
dayMap.forEach((v, dayKey) => {
|
|
1625
|
+
const wk = getIsoWeekKey(dayKey)
|
|
1626
|
+
if (wk === curWeek) total += v
|
|
1627
|
+
})
|
|
1628
|
+
if (total > 0) sums.push({ author, total })
|
|
1629
|
+
})
|
|
1630
|
+
sums.sort((a, b) => b.total - a.total)
|
|
1631
|
+
const top = sums.slice(0, 6)
|
|
1632
|
+
const lines = []
|
|
1633
|
+
lines.push('【本周加班时长风险】')
|
|
1634
|
+
if (top.length === 0) {
|
|
1635
|
+
lines.push('本周暂无加班时长风险。')
|
|
1636
|
+
} else {
|
|
1637
|
+
top.forEach(({ author, total }) => {
|
|
1638
|
+
let level = '轻度'
|
|
1639
|
+
if (total >= 12) level = '严重'
|
|
1640
|
+
else if (total >= 6) level = '中度'
|
|
1641
|
+
lines.push(`${author} 本周累计加班 ${total.toFixed(2)} 小时(${level})。`)
|
|
1642
|
+
})
|
|
1643
|
+
}
|
|
1644
|
+
box.innerHTML = `
|
|
1645
|
+
<div class="risk-summary">
|
|
1646
|
+
<div class="risk-title">【本周加班时长风险】</div>
|
|
1647
|
+
<ul>
|
|
1648
|
+
${lines
|
|
1649
|
+
.slice(1)
|
|
1650
|
+
.map((l) => `<li>${escapeHtml(l)}</li>`)
|
|
1651
|
+
.join('')}
|
|
1652
|
+
</ul>
|
|
1653
|
+
</div>
|
|
1654
|
+
`
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function renderMonthlyDurationRiskSummary(
|
|
1658
|
+
commits,
|
|
1659
|
+
{ startHour = 9, endHour = 18, cutoff = 6 } = {}
|
|
1660
|
+
) {
|
|
1661
|
+
const box = document.getElementById('monthlyDurationRiskSummary')
|
|
1662
|
+
if (!box) return
|
|
1663
|
+
const now = new Date()
|
|
1664
|
+
const curMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
1665
|
+
const byAuthorDay = computeAuthorDailyMaxOvertime(commits, startHour, endHour, cutoff)
|
|
1666
|
+
const sums = []
|
|
1667
|
+
byAuthorDay.forEach((dayMap, author) => {
|
|
1668
|
+
let total = 0
|
|
1669
|
+
dayMap.forEach((v, dayKey) => {
|
|
1670
|
+
const m = dayKey.slice(0, 7)
|
|
1671
|
+
if (m === curMonth) total += v
|
|
1672
|
+
})
|
|
1673
|
+
if (total > 0) sums.push({ author, total })
|
|
1674
|
+
})
|
|
1675
|
+
sums.sort((a, b) => b.total - a.total)
|
|
1676
|
+
const top = sums.slice(0, 6)
|
|
1677
|
+
const lines = []
|
|
1678
|
+
lines.push('【本月加班时长风险】')
|
|
1679
|
+
if (top.length === 0) {
|
|
1680
|
+
lines.push('本月暂无加班时长风险。')
|
|
1681
|
+
} else {
|
|
1682
|
+
top.forEach(({ author, total }) => {
|
|
1683
|
+
let level = '轻度'
|
|
1684
|
+
if (total >= 20) level = '严重'
|
|
1685
|
+
else if (total >= 10) level = '中度'
|
|
1686
|
+
lines.push(`${author} 本月累计加班 ${total.toFixed(2)} 小时(${level})。`)
|
|
1687
|
+
})
|
|
1688
|
+
}
|
|
1689
|
+
box.innerHTML = `
|
|
1690
|
+
<div class="risk-summary">
|
|
1691
|
+
<div class="risk-title">【本月加班时长风险】</div>
|
|
1692
|
+
<ul>
|
|
1693
|
+
${lines
|
|
1694
|
+
.slice(1)
|
|
1695
|
+
.map((l) => `<li>${escapeHtml(l)}</li>`)
|
|
1696
|
+
.join('')}
|
|
1697
|
+
</ul>
|
|
1698
|
+
</div>
|
|
1699
|
+
`
|
|
1700
|
+
}
|
|
1701
|
+
function renderMonthlyRiskSummary(
|
|
1702
|
+
commits,
|
|
1703
|
+
{ startHour = 9, endHour = 18, cutoff = 6 } = {}
|
|
1704
|
+
) {
|
|
1705
|
+
const box = document.getElementById('monthlyRiskSummary')
|
|
1706
|
+
if (!box) return
|
|
1707
|
+
|
|
1708
|
+
const now = new Date()
|
|
1709
|
+
const curKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
1710
|
+
const prev = new Date(now)
|
|
1711
|
+
prev.setMonth(prev.getMonth() - 1)
|
|
1712
|
+
const prevKey = `${prev.getFullYear()}-${String(prev.getMonth() + 1).padStart(2, '0')}`
|
|
1713
|
+
|
|
1714
|
+
const monthAuthor = new Map()
|
|
1715
|
+
const monthMax = new Map()
|
|
1716
|
+
|
|
1717
|
+
commits.forEach((c) => {
|
|
1718
|
+
const d = new Date(c.date)
|
|
1719
|
+
if (Number.isNaN(d.valueOf())) return
|
|
1720
|
+
const h = d.getHours()
|
|
1721
|
+
const isOT = (h >= endHour && h < 24) || (h >= 0 && h < cutoff && h < startHour)
|
|
1722
|
+
if (!isOT) return
|
|
1723
|
+
|
|
1724
|
+
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
|
1725
|
+
const author = c.author || 'unknown'
|
|
1726
|
+
|
|
1727
|
+
if (!monthAuthor.has(key)) monthAuthor.set(key, new Map())
|
|
1728
|
+
const m = monthAuthor.get(key)
|
|
1729
|
+
m.set(author, (m.get(author) || 0) + 1)
|
|
1730
|
+
|
|
1731
|
+
let overtime = null
|
|
1732
|
+
if (h >= endHour && h < 24) overtime = h - endHour
|
|
1733
|
+
else if (h >= 0 && h < cutoff && h < startHour) overtime = 24 - endHour + h
|
|
1734
|
+
if (overtime == null) return
|
|
1735
|
+
|
|
1736
|
+
if (!monthMax.has(key)) monthMax.set(key, new Map())
|
|
1737
|
+
const mm = monthMax.get(key)
|
|
1738
|
+
const cur = mm.get(author)
|
|
1739
|
+
const dateStr = d.toISOString().slice(0, 10)
|
|
1740
|
+
if (!cur || overtime > cur.max) mm.set(author, { max: overtime, date: dateStr })
|
|
1741
|
+
})
|
|
1742
|
+
|
|
1743
|
+
const curMap = monthAuthor.get(curKey) || new Map()
|
|
1744
|
+
const prevMap = monthAuthor.get(prevKey) || new Map()
|
|
1745
|
+
const curTotal = Array.from(curMap.values()).reduce((a, b) => a + b, 0)
|
|
1746
|
+
const prevTotal = Array.from(prevMap.values()).reduce((a, b) => a + b, 0)
|
|
1747
|
+
const delta = prevTotal > 0 ? Math.round(((curTotal - prevTotal) / prevTotal) * 100) : null
|
|
1748
|
+
|
|
1749
|
+
let topAuthor = null
|
|
1750
|
+
let top = { max: -1, date: null }
|
|
1751
|
+
const curMaxMap = monthMax.get(curKey) || new Map()
|
|
1752
|
+
curMaxMap.forEach((v, k) => {
|
|
1753
|
+
if (v.max > top.max) {
|
|
1754
|
+
top = v
|
|
1755
|
+
topAuthor = k
|
|
1756
|
+
}
|
|
1757
|
+
})
|
|
1758
|
+
|
|
1759
|
+
let prevMax = -1
|
|
1760
|
+
const prevMaxMap = monthMax.get(prevKey) || new Map()
|
|
1761
|
+
prevMaxMap.forEach((v) => {
|
|
1762
|
+
if (v.max > prevMax) prevMax = v.max
|
|
1763
|
+
})
|
|
1764
|
+
|
|
1765
|
+
const lines = []
|
|
1766
|
+
lines.push('【本月加班风险】')
|
|
1767
|
+
|
|
1768
|
+
if (curTotal === 0) {
|
|
1769
|
+
lines.push('本月尚无下班后提交,未发现明显风险。')
|
|
1770
|
+
} else {
|
|
1771
|
+
if (delta === null) {
|
|
1772
|
+
lines.push(`本月下班后提交 ${curTotal} 次。`)
|
|
1773
|
+
} else {
|
|
1774
|
+
const trend = delta >= 0 ? '上升' : '下降'
|
|
1775
|
+
lines.push(`本月下班后提交${trend} ${Math.abs(delta)}%(vs 上月)。`)
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
if (top.max >= 0) {
|
|
1779
|
+
let trend2 = '暂无上月对比'
|
|
1780
|
+
if (prevMax >= 0) {
|
|
1781
|
+
if (top.max > prevMax) trend2 = '较上月更晚'
|
|
1782
|
+
else if (top.max < prevMax) trend2 = '较上月提前'
|
|
1783
|
+
else trend2 = '与上月持平'
|
|
1784
|
+
}
|
|
1785
|
+
lines.push(`${topAuthor} 本月最晚超出下班 ${top.max.toFixed(2)} 小时(${top.date}),${trend2}。`)
|
|
1786
|
+
if (top.max >= 2) lines.push('已超过 2 小时,存在严重加班风险。')
|
|
1787
|
+
else if (top.max >= 1) lines.push('已超过 1 小时,存在中度加班风险。')
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
box.innerHTML = `
|
|
1792
|
+
<div class="risk-summary">
|
|
1793
|
+
<div class="risk-title">【本月加班风险】</div>
|
|
1794
|
+
<ul>
|
|
1795
|
+
${lines
|
|
1796
|
+
.slice(1)
|
|
1797
|
+
.map((l) => `<li>${escapeHtml(l)}</li>`)
|
|
1798
|
+
.join('')}
|
|
1799
|
+
</ul>
|
|
1800
|
+
</div>
|
|
1801
|
+
`
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1582
1804
|
// ========= 开发者加班“最晚”趋势(每期取最大超时) =========
|
|
1583
1805
|
function buildAuthorLatestDataset(
|
|
1584
1806
|
commits,
|
|
@@ -1677,6 +1899,7 @@ function drawAuthorLatestOvertimeTrends(commits, stats) {
|
|
|
1677
1899
|
})
|
|
1678
1900
|
|
|
1679
1901
|
renderLatestRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1902
|
+
renderLatestMonthlyRiskSummary(commits, { startHour, endHour, cutoff })
|
|
1680
1903
|
|
|
1681
1904
|
return chart
|
|
1682
1905
|
}
|
|
@@ -1774,6 +1997,89 @@ function renderLatestRiskSummary(
|
|
|
1774
1997
|
`
|
|
1775
1998
|
}
|
|
1776
1999
|
|
|
2000
|
+
function renderLatestMonthlyRiskSummary(
|
|
2001
|
+
commits,
|
|
2002
|
+
{ startHour = 9, endHour = 18, cutoff = 6 } = {}
|
|
2003
|
+
) {
|
|
2004
|
+
const box = document.getElementById('latestMonthlyRiskSummary')
|
|
2005
|
+
if (!box) return
|
|
2006
|
+
|
|
2007
|
+
const now = new Date()
|
|
2008
|
+
const curKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
2009
|
+
const prev = new Date(now)
|
|
2010
|
+
prev.setMonth(prev.getMonth() - 1)
|
|
2011
|
+
const prevKey = `${prev.getFullYear()}-${String(prev.getMonth() + 1).padStart(2, '0')}`
|
|
2012
|
+
|
|
2013
|
+
const monthMax = new Map()
|
|
2014
|
+
commits.forEach((c) => {
|
|
2015
|
+
const d = new Date(c.date)
|
|
2016
|
+
if (Number.isNaN(d.valueOf())) return
|
|
2017
|
+
const h = d.getHours()
|
|
2018
|
+
let overtime = null
|
|
2019
|
+
if (h >= endHour && h < 24) overtime = h - endHour
|
|
2020
|
+
else if (h >= 0 && h < cutoff && h < startHour) overtime = 24 - endHour + h
|
|
2021
|
+
if (overtime == null) return
|
|
2022
|
+
|
|
2023
|
+
const mKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
|
2024
|
+
if (!monthMax.has(mKey)) monthMax.set(mKey, new Map())
|
|
2025
|
+
const m = monthMax.get(mKey)
|
|
2026
|
+
const author = c.author || 'unknown'
|
|
2027
|
+
const cur = m.get(author)
|
|
2028
|
+
if (!cur || overtime > cur.max) {
|
|
2029
|
+
m.set(author, { max: overtime, date: d.toISOString().slice(0, 10) })
|
|
2030
|
+
}
|
|
2031
|
+
})
|
|
2032
|
+
|
|
2033
|
+
const curMap = monthMax.get(curKey) || new Map()
|
|
2034
|
+
const prevMap = monthMax.get(prevKey) || new Map()
|
|
2035
|
+
|
|
2036
|
+
let topAuthor = null
|
|
2037
|
+
let top = { max: -1, date: null }
|
|
2038
|
+
curMap.forEach((v, k) => {
|
|
2039
|
+
if (v.max > top.max) {
|
|
2040
|
+
top = v
|
|
2041
|
+
topAuthor = k
|
|
2042
|
+
}
|
|
2043
|
+
})
|
|
2044
|
+
|
|
2045
|
+
let prevMax = -1
|
|
2046
|
+
prevMap.forEach((v) => {
|
|
2047
|
+
if (v.max > prevMax) prevMax = v.max
|
|
2048
|
+
})
|
|
2049
|
+
|
|
2050
|
+
const lines = []
|
|
2051
|
+
lines.push('【本月最晚加班风险】')
|
|
2052
|
+
|
|
2053
|
+
if (top.max < 0) {
|
|
2054
|
+
lines.push('本月尚无下班后/凌晨提交,未发现明显风险。')
|
|
2055
|
+
} else {
|
|
2056
|
+
let trend = '暂无上月对比'
|
|
2057
|
+
if (prevMax >= 0) {
|
|
2058
|
+
if (top.max > prevMax) trend = '较上月更晚'
|
|
2059
|
+
else if (top.max < prevMax) trend = '较上月提前'
|
|
2060
|
+
else trend = '与上月持平'
|
|
2061
|
+
}
|
|
2062
|
+
lines.push(`${topAuthor} 本月最晚超出下班 ${top.max.toFixed(2)} 小时(${top.date}),${trend}。`)
|
|
2063
|
+
if (top.max >= 2) {
|
|
2064
|
+
lines.push('已超过 2 小时,存在严重加班风险,请关注工作节奏。')
|
|
2065
|
+
} else if (top.max >= 1) {
|
|
2066
|
+
lines.push('已超过 1 小时,注意控制夜间工作时长。')
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
box.innerHTML = `
|
|
2071
|
+
<div class="risk-summary">
|
|
2072
|
+
<div class="risk-title">【本月最晚加班风险】</div>
|
|
2073
|
+
<ul>
|
|
2074
|
+
${lines
|
|
2075
|
+
.slice(1)
|
|
2076
|
+
.map((l) => `<li>${escapeHtml(l)}</li>`)
|
|
2077
|
+
.join('')}
|
|
2078
|
+
</ul>
|
|
2079
|
+
</div>
|
|
2080
|
+
`
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1777
2083
|
async function main() {
|
|
1778
2084
|
const {
|
|
1779
2085
|
commits,
|
package/web/index.html
CHANGED
|
@@ -83,6 +83,9 @@
|
|
|
83
83
|
</div>
|
|
84
84
|
<div id="chartAuthorOvertime" class="echart"></div>
|
|
85
85
|
<div id="weeklyRiskSummary" class="risk-summary-box"></div>
|
|
86
|
+
<div id="monthlyRiskSummary" class="risk-summary-box"></div>
|
|
87
|
+
<div id="weeklyDurationRiskSummary" class="risk-summary-box"></div>
|
|
88
|
+
<div id="monthlyDurationRiskSummary" class="risk-summary-box"></div>
|
|
86
89
|
</div>
|
|
87
90
|
|
|
88
91
|
<div class="chart-card">
|
|
@@ -94,6 +97,7 @@
|
|
|
94
97
|
</div>
|
|
95
98
|
<div id="chartAuthorLatestOvertime" class="echart"></div>
|
|
96
99
|
<div id="latestRiskSummary" class="risk-summary-box"></div>
|
|
100
|
+
<div id="latestMonthlyRiskSummary" class="risk-summary-box"></div>
|
|
97
101
|
</div>
|
|
98
102
|
|
|
99
103
|
<section class="table-card">
|