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 技能的日誌頁週次。
This commit is contained in:
2026-08-31 11:07:16 +08:00
parent 7d803245a5
commit 1bec87b047
2 changed files with 193 additions and 2 deletions
+150
View File
@@ -0,0 +1,150 @@
#!/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
+43 -2
View File
@@ -1,18 +1,59 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# worklog-target.sh: 由已算好的 hash 組出 worklog wiki 目標頁名稱。 # worklog-target.sh: 由已算好的 hash 組出 worklog wiki 目標頁名稱,並算出工作週的週五。
# hash 一律由 jsc-gitea/tools/hash-id 算出後傳入,本腳本不再自行計算 SHA-1。 # hash 一律由 jsc-gitea/tools/hash-id 算出後傳入,本腳本不再自行計算 SHA-1。
# 用法: # 用法:
# worklog-target.sh <hash> [page|contents|all] # worklog-target.sh <hash> [page|contents|all]
# worklog-target.sh friday [基準日期 yyyy-MM-dd]
#
# friday 印出一行 yyyy-MM-dd:基準日期所在工作週的週五(基準日期省略時用今天)。
# 週界不在這裡算,一律問 report-range.sh weekly——週一起算的 ISO 週定義只有那一支說了算,
# 兩邊各算一次就會在跨月、跨年的那一週各走各的。這裡只做「週一加四天」這一步。
#
# 結束碼: 0=成功 2=用法錯誤 4=系統的 date 不支援日期運算(由 report-range.sh 判定)
set -eu set -eu
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
usage() { usage() {
echo 'usage: worklog-target.sh <hash> [page|contents|all]' >&2 cat >&2 <<'EOF'
用法:
worklog-target.sh <hash> [page|contents|all] 組出 LOG_{HASH} 與 LOG_CONTENTS
worklog-target.sh friday [yyyy-MM-dd] 印出該工作週的週五日期
結束碼: 0=成功 2=用法錯誤 4=系統的 date 不支援日期運算
EOF
exit 2 exit 2
} }
add_days() { # $1=yyyy-MM-dd $2=要加的天數
if date -d "$1 +$2 days" +%Y-%m-%d >/dev/null 2>&1; then
date -d "$1 +$2 days" +%Y-%m-%d
return 0
fi
if date -j -f %Y-%m-%d -v"+$2d" "$1" +%Y-%m-%d >/dev/null 2>&1; then
date -j -f %Y-%m-%d -v"+$2d" "$1" +%Y-%m-%d
return 0
fi
echo "系統的 date 不支援日期運算,算不出工作週的週五" >&2
exit 4
}
[ "$#" -ge 1 ] || usage [ "$#" -ge 1 ] || usage
[ "$#" -le 2 ] || usage [ "$#" -le 2 ] || usage
if [ "$1" = friday ]; then
base="${2:-}"
if [ -n "$base" ]; then
case "$base" in
[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;;
*) echo "基準日期格式要是 yyyy-MM-dd:$base" >&2; exit 2 ;;
esac
fi
range=$(sh "$HERE/report-range.sh" weekly ${base:+"$base"}) || exit $?
monday=$(printf '%s\n' "$range" | cut -f1)
add_days "$monday" 4
exit 0
fi
hash=$1 hash=$1
field=${2:-page} field=${2:-page}