Files
log/tools/log-aggregate.sh
jiantw83 1bec87b047 feat(tools): 報表彙總與工作週週五都交給腳本算
- What:新增 tools/log-aggregate.sh,把日誌頁彙總成報表數字;
  tools/worklog-target.sh 新增 friday 子命令,印出該工作週的週五。
- Why:彙總與日期原本只寫在技能散文裡,每次跑都靠模型自己數。
  四個條目只有一個填了花費時間時,模型容易把它攤到每一筆,得出一個沒人做過的工時。
  週五同理,跨月、跨年那一週用手算就會差一天。規則寫進程式,每次跑才會一樣。
- How:log-aggregate.sh 讀日誌條目的固定版型,依條目標題的日期取範圍內的條目,
  印出條目數、存取庫、花費時間、各 CLI 的 token 用量與狀態計數。
  「無資料不估算」寫死在程式裡:沒填時間的條目不進總和,只進 ELAPSED_MISSING;
  整段期間都沒有時間就印「無資料」,不印 0,因為 0 會被讀成「花了 0 分鐘」。
  friday 的週界向 report-range.sh weekly 取得,這裡只做「週一加四天」,不自己定義週的起點。
- Who:report 技能的數字彙總,worklog 技能的日誌頁週次。
2026-08-31 11:07:16 +08:00

151 lines
5.7 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# log-aggregate.sh — 把工作日誌頁的條目彙總成報表數字(供 jsc-log:report 呼叫)。
#
# 用法:
# log-aggregate.sh <起 yyyy-MM-dd> <訖 yyyy-MM-dd> [日誌頁檔案 ...]
# 檔案省略時讀標準輸入。可以一次傳多份 LOG_{HASH} 的內容,數字會跨頁加總。
#
# 輸入格式即 templates/log-entry.md 的固定版型:
# 條目標題「## {yyyy-MM-dd HH:mm} {摘要}」,底下是「| 欄位 | 內容 |」表格。
# 只認四個欄位:存取庫名稱、花費時間、token 用量、任務狀態。
# 起訖含頭含尾,比對的是條目標題那個日期。
#
# 輸出(一行一項,KEY=值;值有多欄時以 TAB 分隔):
# ENTRIES=<期間內條目數>
# REPOS=<相異存取庫數>
# REPO=<owner/repo> 每個存取庫一行,依字典序
# ELAPSED_MINUTES=<分鐘數|無資料> 有填花費時間的條目才加總
# ELAPSED_ENTRIES=<有填花費時間的條目數>
# ELAPSED_MISSING=<沒填花費時間的條目數>
# TOKEN=<cli><TAB><input><TAB><output> 每個 CLI 一行,依 CLI 名字典序
# TOKEN_MISSING=<token 欄位無數字的條目數>
# STATUS=<狀態><TAB><條目數> 每個狀態一行,依字典序
#
# 無資料不估算(本腳本的硬規則,不是散文建議):
# 花費時間欄空白、填「無」或「無資料」的條目,不進 ELAPSED_MINUTES,只進 ELAPSED_MISSING。
# 四個條目只有一個有時間時,總和就是那一個的時間,不乘四也不取平均。
# token 欄填 N/A 的條目同理,只進 TOKEN_MISSING。
# 全部條目都沒有時間時,ELAPSED_MINUTES 印「無資料」,不印 0——0 會被讀成「花了 0 分鐘」。
#
# 結束碼: 0=成功 2=用法錯誤 3=期間內沒有條目(仍印出全零結果)4=讀不到輸入檔
set -u
usage() {
echo '用法:log-aggregate.sh <起 yyyy-MM-dd> <訖 yyyy-MM-dd> [日誌頁檔案 ...]' >&2
exit 2
}
start="${1:-}"; [ -n "$start" ] || usage
shift
end="${1:-}"; [ -n "$end" ] || usage
shift
for d in "$start" "$end"; do
case "$d" in
[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;;
*) echo "日期格式要是 yyyy-MM-dd:$d" >&2; exit 2 ;;
esac
done
if [ "$(printf '%s\n%s\n' "$start" "$end" | sort | head -n1)" != "$start" ]; then
echo "起始日期比結束日期晚:$start 到 $end" >&2
exit 2
fi
for f in "$@"; do
[ -r "$f" ] || { echo "讀不到日誌頁檔案:$f" >&2; exit 4; }
done
aggregate() {
awk -v start="$start" -v end="$end" '
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
function sortkeys(arr, out, i, j, n, t, k) {
n = 0
for (k in arr) { n++; out[n] = k }
for (i = 1; i < n; i++)
for (j = i + 1; j <= n; j++)
if (out[j] < out[i]) { t = out[i]; out[i] = out[j]; out[j] = t }
return n
}
# 花費時間欄轉分鐘。認得「{h} 小時 {m} 分」、「{h} 小時」、「{m} 分」三種寫法。
# 一個數字都抓不到就回 -1,交給呼叫端算成「無資料」,不猜。
function minutes(v, h, m, tmp) {
h = -1; m = -1
tmp = v
if (match(tmp, /[0-9]+[ ]*小時/)) { h = substr(tmp, RSTART, RLENGTH) + 0 }
if (match(tmp, /[0-9]+[ ]*分/)) { m = substr(tmp, RSTART, RLENGTH) + 0 }
if (h < 0 && m < 0) return -1
if (h < 0) h = 0
if (m < 0) m = 0
return h * 60 + m
}
# 狀態只認三個字面值,其餘一律歸「其他」。先比對「部分完成」,它包含「完成」。
function status_of(v) {
if (index(v, "部分完成") > 0) return "部分完成"
if (index(v, "阻塞") > 0) return "阻塞"
if (index(v, "完成") > 0) return "完成"
return "其他"
}
BEGIN {
FS = "|"
entries = 0; repos_n = 0
elapsed_total = 0; elapsed_entries = 0; elapsed_missing = 0
token_missing = 0
in_range = 0
}
/^##[ \t]/ {
in_range = 0
if (match($0, /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/)) {
d = substr($0, RSTART, RLENGTH)
if (d >= start && d <= end) { in_range = 1; entries++ }
}
next
}
in_range == 1 && /^[ \t]*\|/ {
key = trim($2); val = trim($3)
if (key == "存取庫名稱") {
if (val != "" && val !~ /^\{/) { if (!(val in repos)) { repos[val] = 1; repos_n++ } }
} else if (key == "花費時間") {
mm = minutes(val)
if (mm < 0) { elapsed_missing++ } else { elapsed_total += mm; elapsed_entries++ }
} else if (key == "token 用量" || key == "Token 用量") {
rest = val; got = 0
while (match(rest, /[A-Za-z][A-Za-z0-9_-]*[ ]*:[ ]*[0-9]+[ ]*\/[ ]*[0-9]+/)) {
pair = substr(rest, RSTART, RLENGTH)
rest = substr(rest, RSTART + RLENGTH)
split(pair, a, ":")
cli = trim(a[1])
split(a[2], b, "/")
tin[cli] += trim(b[1]) + 0
tout[cli] += trim(b[2]) + 0
got = 1
}
if (got == 0) token_missing++
} else if (key == "任務狀態") {
if (val != "" && val !~ /^\{/) { st[status_of(val)]++ }
}
}
END {
printf "ENTRIES=%d\n", entries
printf "REPOS=%d\n", repos_n
n = sortkeys(repos, sorted)
for (i = 1; i <= n; i++) printf "REPO=%s\n", sorted[i]
if (elapsed_entries > 0) printf "ELAPSED_MINUTES=%d\n", elapsed_total
else printf "ELAPSED_MINUTES=%s\n", "無資料"
printf "ELAPSED_ENTRIES=%d\n", elapsed_entries
printf "ELAPSED_MISSING=%d\n", elapsed_missing
n = sortkeys(tin, sorted)
for (i = 1; i <= n; i++) printf "TOKEN=%s\t%d\t%d\n", sorted[i], tin[sorted[i]], tout[sorted[i]]
printf "TOKEN_MISSING=%d\n", token_missing
n = sortkeys(st, sorted)
for (i = 1; i <= n; i++) printf "STATUS=%s\t%d\n", sorted[i], st[sorted[i]]
exit (entries > 0 ? 0 : 3)
}
' "$@"
}
if [ "$#" -gt 0 ]; then
aggregate "$@"
else
aggregate -
fi