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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
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,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">