wukong-gitlog-cli 1.0.18 → 1.0.20

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 CHANGED
@@ -2,6 +2,21 @@
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.20](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.19...v1.0.20) (2025-12-05)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 rolling30DurationRiskSummary ([b4a976e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/b4a976e552cfdc1483b0f23f2a4126ecede42af3))
11
+
12
+ ### [1.0.19](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.18...v1.0.19) (2025-12-05)
13
+
14
+
15
+ ### Features
16
+
17
+ * 🎸 latestMonthlyRiskSummary ([e8a442e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e8a442e8333ac303815e7eaaf3aa571e7e976bd6))
18
+ * 🎸 monthlyDurationRiskSummary ([d853876](https://github.com/tomatobybike/wukong-gitlog-cli/commit/d853876a5457bac57fbe4a000be90308b49d8b9d))
19
+
5
20
  ### [1.0.18](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.17...v1.0.18) (2025-12-05)
6
21
 
7
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
package/web/app.js CHANGED
@@ -1480,6 +1480,10 @@ 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 })
1486
+ renderRolling30DurationRiskSummary(commits, { startHour, endHour, cutoff })
1483
1487
 
1484
1488
  return chart
1485
1489
  }
@@ -1579,6 +1583,272 @@ function renderWeeklyRiskSummary(
1579
1583
  `
1580
1584
  }
1581
1585
 
1586
+ function computeAuthorDailyMaxOvertime(commits, startHour, endHour, cutoff) {
1587
+ const byAuthorDay = new Map()
1588
+ commits.forEach((c) => {
1589
+ const d = new Date(c.date)
1590
+ if (Number.isNaN(d.valueOf())) return
1591
+ const h = d.getHours()
1592
+ let overtime = null
1593
+ let dayKey = null
1594
+ if (h >= endHour && h < 24) {
1595
+ overtime = h - endHour
1596
+ dayKey = d.toISOString().slice(0, 10)
1597
+ } else if (h >= 0 && h < cutoff && h < startHour) {
1598
+ overtime = 24 - endHour + h
1599
+ const cur = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()))
1600
+ cur.setUTCDate(cur.getUTCDate() - 1)
1601
+ dayKey = cur.toISOString().slice(0, 10)
1602
+ }
1603
+ if (overtime == null || !dayKey) return
1604
+ const author = c.author || 'unknown'
1605
+ if (!byAuthorDay.has(author)) byAuthorDay.set(author, new Map())
1606
+ const m = byAuthorDay.get(author)
1607
+ const cur = m.get(dayKey)
1608
+ if (!cur || overtime > cur) m.set(dayKey, overtime)
1609
+ })
1610
+ return byAuthorDay
1611
+ }
1612
+
1613
+ function renderWeeklyDurationRiskSummary(
1614
+ commits,
1615
+ { startHour = 9, endHour = 18, cutoff = 6 } = {}
1616
+ ) {
1617
+ const box = document.getElementById('weeklyDurationRiskSummary')
1618
+ if (!box) return
1619
+ const now = new Date()
1620
+ const curWeek = getIsoWeekKey(now.toISOString().slice(0, 10))
1621
+ const byAuthorDay = computeAuthorDailyMaxOvertime(commits, startHour, endHour, cutoff)
1622
+ const sums = []
1623
+ byAuthorDay.forEach((dayMap, author) => {
1624
+ let total = 0
1625
+ dayMap.forEach((v, dayKey) => {
1626
+ const wk = getIsoWeekKey(dayKey)
1627
+ if (wk === curWeek) total += v
1628
+ })
1629
+ if (total > 0) sums.push({ author, total })
1630
+ })
1631
+ sums.sort((a, b) => b.total - a.total)
1632
+ const top = sums.slice(0, 6)
1633
+ const lines = []
1634
+ lines.push('【本周加班时长风险】')
1635
+ if (top.length === 0) {
1636
+ lines.push('本周暂无加班时长风险。')
1637
+ } else {
1638
+ top.forEach(({ author, total }) => {
1639
+ let level = '轻度'
1640
+ if (total >= 12) level = '严重'
1641
+ else if (total >= 6) level = '中度'
1642
+ lines.push(`${author} 本周累计加班 ${total.toFixed(2)} 小时(${level})。`)
1643
+ })
1644
+ }
1645
+ box.innerHTML = `
1646
+ <div class="risk-summary">
1647
+ <div class="risk-title">【本周加班时长风险】</div>
1648
+ <ul>
1649
+ ${lines
1650
+ .slice(1)
1651
+ .map((l) => `<li>${escapeHtml(l)}</li>`)
1652
+ .join('')}
1653
+ </ul>
1654
+ </div>
1655
+ `
1656
+ }
1657
+
1658
+ function renderMonthlyDurationRiskSummary(
1659
+ commits,
1660
+ { startHour = 9, endHour = 18, cutoff = 6 } = {}
1661
+ ) {
1662
+ const box = document.getElementById('monthlyDurationRiskSummary')
1663
+ if (!box) return
1664
+ const now = new Date()
1665
+ const curMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
1666
+ const byAuthorDay = computeAuthorDailyMaxOvertime(commits, startHour, endHour, cutoff)
1667
+ const sums = []
1668
+ byAuthorDay.forEach((dayMap, author) => {
1669
+ let total = 0
1670
+ dayMap.forEach((v, dayKey) => {
1671
+ const m = dayKey.slice(0, 7)
1672
+ if (m === curMonth) total += v
1673
+ })
1674
+ if (total > 0) sums.push({ author, total })
1675
+ })
1676
+ sums.sort((a, b) => b.total - a.total)
1677
+ const top = sums.slice(0, 6)
1678
+ const lines = []
1679
+ lines.push('【本月加班时长风险】')
1680
+ if (top.length === 0) {
1681
+ lines.push('本月暂无加班时长风险。')
1682
+ } else {
1683
+ top.forEach(({ author, total }) => {
1684
+ let level = '轻度'
1685
+ if (total >= 20) level = '严重'
1686
+ else if (total >= 10) level = '中度'
1687
+ lines.push(`${author} 本月累计加班 ${total.toFixed(2)} 小时(${level})。`)
1688
+ })
1689
+ }
1690
+ box.innerHTML = `
1691
+ <div class="risk-summary">
1692
+ <div class="risk-title">【本月加班时长风险】</div>
1693
+ <ul>
1694
+ ${lines
1695
+ .slice(1)
1696
+ .map((l) => `<li>${escapeHtml(l)}</li>`)
1697
+ .join('')}
1698
+ </ul>
1699
+ </div>
1700
+ `
1701
+ }
1702
+
1703
+ function renderRolling30DurationRiskSummary(
1704
+ commits,
1705
+ { startHour = 9, endHour = 18, cutoff = 6 } = {}
1706
+ ) {
1707
+ const box = document.getElementById('rolling30DurationRiskSummary')
1708
+ if (!box) return
1709
+ const now = new Date()
1710
+ const utcToday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
1711
+ utcToday.setUTCDate(utcToday.getUTCDate() - 29)
1712
+ const startKey = utcToday.toISOString().slice(0, 10)
1713
+
1714
+ const byAuthorDay = computeAuthorDailyMaxOvertime(commits, startHour, endHour, cutoff)
1715
+ const sums = []
1716
+ byAuthorDay.forEach((dayMap, author) => {
1717
+ let total = 0
1718
+ dayMap.forEach((v, dayKey) => {
1719
+ if (dayKey >= startKey) total += v
1720
+ })
1721
+ if (total > 0) sums.push({ author, total })
1722
+ })
1723
+ sums.sort((a, b) => b.total - a.total)
1724
+ const top = sums.slice(0, 6)
1725
+ const lines = []
1726
+ lines.push('【最近30天加班时长风险】')
1727
+ if (top.length === 0) {
1728
+ lines.push('最近30天暂无加班时长风险。')
1729
+ } else {
1730
+ top.forEach(({ author, total }) => {
1731
+ let level = '轻度'
1732
+ if (total >= 20) level = '严重'
1733
+ else if (total >= 10) level = '中度'
1734
+ lines.push(`${author} 最近30天累计加班 ${total.toFixed(2)} 小时(${level})。`)
1735
+ })
1736
+ }
1737
+ box.innerHTML = `
1738
+ <div class="risk-summary">
1739
+ <div class="risk-title">【最近30天加班时长风险】</div>
1740
+ <ul>
1741
+ ${lines
1742
+ .slice(1)
1743
+ .map((l) => `<li>${escapeHtml(l)}</li>`)
1744
+ .join('')}
1745
+ </ul>
1746
+ </div>
1747
+ `
1748
+ }
1749
+ function renderMonthlyRiskSummary(
1750
+ commits,
1751
+ { startHour = 9, endHour = 18, cutoff = 6 } = {}
1752
+ ) {
1753
+ const box = document.getElementById('monthlyRiskSummary')
1754
+ if (!box) return
1755
+
1756
+ const now = new Date()
1757
+ const curKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
1758
+ const prev = new Date(now)
1759
+ prev.setMonth(prev.getMonth() - 1)
1760
+ const prevKey = `${prev.getFullYear()}-${String(prev.getMonth() + 1).padStart(2, '0')}`
1761
+
1762
+ const monthAuthor = new Map()
1763
+ const monthMax = new Map()
1764
+
1765
+ commits.forEach((c) => {
1766
+ const d = new Date(c.date)
1767
+ if (Number.isNaN(d.valueOf())) return
1768
+ const h = d.getHours()
1769
+ const isOT = (h >= endHour && h < 24) || (h >= 0 && h < cutoff && h < startHour)
1770
+ if (!isOT) return
1771
+
1772
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
1773
+ const author = c.author || 'unknown'
1774
+
1775
+ if (!monthAuthor.has(key)) monthAuthor.set(key, new Map())
1776
+ const m = monthAuthor.get(key)
1777
+ m.set(author, (m.get(author) || 0) + 1)
1778
+
1779
+ let overtime = null
1780
+ if (h >= endHour && h < 24) overtime = h - endHour
1781
+ else if (h >= 0 && h < cutoff && h < startHour) overtime = 24 - endHour + h
1782
+ if (overtime == null) return
1783
+
1784
+ if (!monthMax.has(key)) monthMax.set(key, new Map())
1785
+ const mm = monthMax.get(key)
1786
+ const cur = mm.get(author)
1787
+ const dateStr = d.toISOString().slice(0, 10)
1788
+ if (!cur || overtime > cur.max) mm.set(author, { max: overtime, date: dateStr })
1789
+ })
1790
+
1791
+ const curMap = monthAuthor.get(curKey) || new Map()
1792
+ const prevMap = monthAuthor.get(prevKey) || new Map()
1793
+ const curTotal = Array.from(curMap.values()).reduce((a, b) => a + b, 0)
1794
+ const prevTotal = Array.from(prevMap.values()).reduce((a, b) => a + b, 0)
1795
+ const delta = prevTotal > 0 ? Math.round(((curTotal - prevTotal) / prevTotal) * 100) : null
1796
+
1797
+ let topAuthor = null
1798
+ let top = { max: -1, date: null }
1799
+ const curMaxMap = monthMax.get(curKey) || new Map()
1800
+ curMaxMap.forEach((v, k) => {
1801
+ if (v.max > top.max) {
1802
+ top = v
1803
+ topAuthor = k
1804
+ }
1805
+ })
1806
+
1807
+ let prevMax = -1
1808
+ const prevMaxMap = monthMax.get(prevKey) || new Map()
1809
+ prevMaxMap.forEach((v) => {
1810
+ if (v.max > prevMax) prevMax = v.max
1811
+ })
1812
+
1813
+ const lines = []
1814
+ lines.push('【本月加班风险】')
1815
+
1816
+ if (curTotal === 0) {
1817
+ lines.push('本月尚无下班后提交,未发现明显风险。')
1818
+ } else {
1819
+ if (delta === null) {
1820
+ lines.push(`本月下班后提交 ${curTotal} 次。`)
1821
+ } else {
1822
+ const trend = delta >= 0 ? '上升' : '下降'
1823
+ lines.push(`本月下班后提交${trend} ${Math.abs(delta)}%(vs 上月)。`)
1824
+ }
1825
+
1826
+ if (top.max >= 0) {
1827
+ let trend2 = '暂无上月对比'
1828
+ if (prevMax >= 0) {
1829
+ if (top.max > prevMax) trend2 = '较上月更晚'
1830
+ else if (top.max < prevMax) trend2 = '较上月提前'
1831
+ else trend2 = '与上月持平'
1832
+ }
1833
+ lines.push(`${topAuthor} 本月最晚超出下班 ${top.max.toFixed(2)} 小时(${top.date}),${trend2}。`)
1834
+ if (top.max >= 2) lines.push('已超过 2 小时,存在严重加班风险。')
1835
+ else if (top.max >= 1) lines.push('已超过 1 小时,存在中度加班风险。')
1836
+ }
1837
+ }
1838
+
1839
+ box.innerHTML = `
1840
+ <div class="risk-summary">
1841
+ <div class="risk-title">【本月加班风险】</div>
1842
+ <ul>
1843
+ ${lines
1844
+ .slice(1)
1845
+ .map((l) => `<li>${escapeHtml(l)}</li>`)
1846
+ .join('')}
1847
+ </ul>
1848
+ </div>
1849
+ `
1850
+ }
1851
+
1582
1852
  // ========= 开发者加班“最晚”趋势(每期取最大超时) =========
1583
1853
  function buildAuthorLatestDataset(
1584
1854
  commits,
@@ -1677,6 +1947,7 @@ function drawAuthorLatestOvertimeTrends(commits, stats) {
1677
1947
  })
1678
1948
 
1679
1949
  renderLatestRiskSummary(commits, { startHour, endHour, cutoff })
1950
+ renderLatestMonthlyRiskSummary(commits, { startHour, endHour, cutoff })
1680
1951
 
1681
1952
  return chart
1682
1953
  }
@@ -1774,6 +2045,89 @@ function renderLatestRiskSummary(
1774
2045
  `
1775
2046
  }
1776
2047
 
2048
+ function renderLatestMonthlyRiskSummary(
2049
+ commits,
2050
+ { startHour = 9, endHour = 18, cutoff = 6 } = {}
2051
+ ) {
2052
+ const box = document.getElementById('latestMonthlyRiskSummary')
2053
+ if (!box) return
2054
+
2055
+ const now = new Date()
2056
+ const curKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
2057
+ const prev = new Date(now)
2058
+ prev.setMonth(prev.getMonth() - 1)
2059
+ const prevKey = `${prev.getFullYear()}-${String(prev.getMonth() + 1).padStart(2, '0')}`
2060
+
2061
+ const monthMax = new Map()
2062
+ commits.forEach((c) => {
2063
+ const d = new Date(c.date)
2064
+ if (Number.isNaN(d.valueOf())) return
2065
+ const h = d.getHours()
2066
+ let overtime = null
2067
+ if (h >= endHour && h < 24) overtime = h - endHour
2068
+ else if (h >= 0 && h < cutoff && h < startHour) overtime = 24 - endHour + h
2069
+ if (overtime == null) return
2070
+
2071
+ const mKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
2072
+ if (!monthMax.has(mKey)) monthMax.set(mKey, new Map())
2073
+ const m = monthMax.get(mKey)
2074
+ const author = c.author || 'unknown'
2075
+ const cur = m.get(author)
2076
+ if (!cur || overtime > cur.max) {
2077
+ m.set(author, { max: overtime, date: d.toISOString().slice(0, 10) })
2078
+ }
2079
+ })
2080
+
2081
+ const curMap = monthMax.get(curKey) || new Map()
2082
+ const prevMap = monthMax.get(prevKey) || new Map()
2083
+
2084
+ let topAuthor = null
2085
+ let top = { max: -1, date: null }
2086
+ curMap.forEach((v, k) => {
2087
+ if (v.max > top.max) {
2088
+ top = v
2089
+ topAuthor = k
2090
+ }
2091
+ })
2092
+
2093
+ let prevMax = -1
2094
+ prevMap.forEach((v) => {
2095
+ if (v.max > prevMax) prevMax = v.max
2096
+ })
2097
+
2098
+ const lines = []
2099
+ lines.push('【本月最晚加班风险】')
2100
+
2101
+ if (top.max < 0) {
2102
+ lines.push('本月尚无下班后/凌晨提交,未发现明显风险。')
2103
+ } else {
2104
+ let trend = '暂无上月对比'
2105
+ if (prevMax >= 0) {
2106
+ if (top.max > prevMax) trend = '较上月更晚'
2107
+ else if (top.max < prevMax) trend = '较上月提前'
2108
+ else trend = '与上月持平'
2109
+ }
2110
+ lines.push(`${topAuthor} 本月最晚超出下班 ${top.max.toFixed(2)} 小时(${top.date}),${trend}。`)
2111
+ if (top.max >= 2) {
2112
+ lines.push('已超过 2 小时,存在严重加班风险,请关注工作节奏。')
2113
+ } else if (top.max >= 1) {
2114
+ lines.push('已超过 1 小时,注意控制夜间工作时长。')
2115
+ }
2116
+ }
2117
+
2118
+ box.innerHTML = `
2119
+ <div class="risk-summary">
2120
+ <div class="risk-title">【本月最晚加班风险】</div>
2121
+ <ul>
2122
+ ${lines
2123
+ .slice(1)
2124
+ .map((l) => `<li>${escapeHtml(l)}</li>`)
2125
+ .join('')}
2126
+ </ul>
2127
+ </div>
2128
+ `
2129
+ }
2130
+
1777
2131
  async function main() {
1778
2132
  const {
1779
2133
  commits,
package/web/index.html CHANGED
@@ -83,6 +83,10 @@
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>
89
+ <div id="rolling30DurationRiskSummary" class="risk-summary-box"></div>
86
90
  </div>
87
91
 
88
92
  <div class="chart-card">
@@ -94,6 +98,7 @@
94
98
  </div>
95
99
  <div id="chartAuthorLatestOvertime" class="echart"></div>
96
100
  <div id="latestRiskSummary" class="risk-summary-box"></div>
101
+ <div id="latestMonthlyRiskSummary" class="risk-summary-box"></div>
97
102
  </div>
98
103
 
99
104
  <section class="table-card">