feat(report): 新增工作日誌總結技能

What: 新增 jsc-log:report,把工作日誌總結成年報、月報、週報或日報,寫進 wiki REPORT_{HASH}。另加 tools/report-range.sh 算期間、tools/report-template.sh 解析範本,以及四份報表範本與報表目錄範本。
Why: worklog 只記單筆工作,累積起來沒有工具看得出一週或一個月做了什麼、什麼在卡。要總結只能自己翻頁面加總。
How: 期間換算交給系統的 date,不自己算閏年與週界;週次採 ISO-8601。範本解析採工作目錄優先,.jsc/templates/report-{period}.md 蓋過技能自帶的那份,專案要換格式不必改技能。資料讀 LOG_CONTENTS 列出的所有日誌頁,取工作日期落在期間內的條目,只加總條目真的寫了的數字。
Who: 工作日誌的期間總結。
This commit is contained in:
2026-08-26 10:47:07 +08:00
parent b1baaf1546
commit e6fafd4132
12 changed files with 388 additions and 4 deletions
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env sh
# report-range.sh — 算出報表期間的起訖日期與標籤(供 jsc-log:report 呼叫)。
# 用法:
# report-range.sh {daily|weekly|monthly|yearly} [基準日期 yyyy-MM-dd]
# 基準日期省略時用今天。輸出一行 TSV:
# {start}<TAB>{end}<TAB>{label}<TAB>{period}
# start、end 皆為 yyyy-MM-dd,含頭含尾。
# label:daily=2026-08-26、weekly=2026-W35、monthly=2026-08、yearly=2026
#
# 週的定義採 ISO-8601:週一起算、週日結束,標籤用 ISO 週次。worklog 頁內以工作週的
# 週五整理內容,週次與那個週五落在同一週,兩邊對得起來。
#
# 日期運算優先用 GNU date -d,退回 BSD date -v。兩者都不支援就 exit 4,不自己算閏年:
# 手刻日期運算出錯的方式很安靜,報表會少算一天而沒有人發現。
#
# 結束碼: 0=成功 2=用法錯誤 4=系統的 date 不支援日期運算
set -eu
usage() {
echo "用法:report-range.sh {daily|weekly|monthly|yearly} [yyyy-MM-dd]" >&2
exit 2
}
period="${1:-}"
case "$period" in daily|weekly|monthly|yearly) ;; *) usage ;; esac
base="${2:-}"
if [ -z "$base" ]; then
base=$(date +%Y-%m-%d)
else
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
# $1=基準日 $2=偏移(例 -3 days、+1 month)$3=輸出格式
shift_date() {
if date -d "$1 $2" "+$3" >/dev/null 2>&1; then
date -d "$1 $2" "+$3"
return 0
fi
# BSD:偏移寫法不同(-v-3d),只轉換這裡真的用到的幾種
_bsd=$(printf '%s' "$2" | sed 's/ days\{0,1\}/d/; s/ months\{0,1\}/m/; s/ years\{0,1\}/y/; s/^+/+/')
if date -j -f %Y-%m-%d -v"$_bsd" "$1" "+$3" >/dev/null 2>&1; then
date -j -f %Y-%m-%d -v"$_bsd" "$1" "+$3"
return 0
fi
echo "系統的 date 不支援日期運算,算不出報表期間" >&2
exit 4
}
fmt() { shift_date "$1" "+0 days" "$2"; }
case "$period" in
daily)
start="$base"; end="$base"
label=$(fmt "$base" %Y-%m-%d) ;;
weekly)
# ISO 週:週一為第 1 天。%u 給 1(週一)到 7(週日)。
dow=$(fmt "$base" %u)
start=$(shift_date "$base" "-$((dow - 1)) days" %Y-%m-%d)
end=$(shift_date "$start" "+6 days" %Y-%m-%d)
label=$(fmt "$base" %G-W%V) ;;
monthly)
start=$(fmt "$base" %Y-%m-01)
end=$(shift_date "$(shift_date "$start" "+1 month" %Y-%m-%d)" "-1 day" %Y-%m-%d)
label=$(fmt "$base" %Y-%m) ;;
yearly)
start=$(fmt "$base" %Y-01-01)
end=$(fmt "$base" %Y-12-31)
label=$(fmt "$base" %Y) ;;
esac
printf '%s\t%s\t%s\t%s\n' "$start" "$end" "$label" "$period"
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env sh
# report-template.sh — 解析報表範本的位置(供 jsc-log:report 呼叫)。
# 用法:
# report-template.sh resolve {daily|weekly|monthly|yearly} [起始目錄]
# report-template.sh list [起始目錄]
#
# 解析順序:工作目錄的 .jsc/templates/report-{period}.md 優先,沒有才用技能自帶的
# templates/report-{period}.md。專案要自訂報表格式時,放一份同名檔案就換掉範本,
# 不必改技能。起始目錄省略時用目前工作目錄。
#
# 輸出(resolve):{絕對路徑}<TAB>{project|skill}
# 輸出(list):每種期間一行 {period}<TAB>{絕對路徑}<TAB>{project|skill}
#
# 結束碼: 0=找到 2=用法錯誤 3=兩處都找不到範本(技能自帶的那份被刪了)
set -eu
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
SKILL_TPL=$(CDPATH= cd -- "$HERE/.." && pwd)/templates
TAB=$(printf '\t')
usage() {
echo "用法:report-template.sh {resolve {daily|weekly|monthly|yearly} [起始目錄]|list [起始目錄]}" >&2
exit 2
}
resolve_one() { # $1=period $2=起始目錄
_p="$1"; _d="$2"
_proj="$_d/.jsc/templates/report-$_p.md"
if [ -f "$_proj" ]; then
printf '%s%s%s\n' "$_proj" "$TAB" project
return 0
fi
_skill="$SKILL_TPL/report-$_p.md"
if [ -f "$_skill" ]; then
printf '%s%s%s\n' "$_skill" "$TAB" skill
return 0
fi
return 3
}
cmd="${1:-}"
case "$cmd" in
resolve)
period="${2:-}"
case "$period" in daily|weekly|monthly|yearly) ;; *) usage ;; esac
dir="${3:-$PWD}"
dir=$(CDPATH= cd -- "$dir" 2>/dev/null && pwd) || { echo "找不到目錄:${3:-$PWD}" >&2; exit 2; }
resolve_one "$period" "$dir" || {
echo "找不到 report-$period.md:$dir/.jsc/templates/ 與 $SKILL_TPL 都沒有" >&2
exit 3
} ;;
list)
dir="${2:-$PWD}"
dir=$(CDPATH= cd -- "$dir" 2>/dev/null && pwd) || { echo "找不到目錄:${2:-$PWD}" >&2; exit 2; }
rc=0
for p in daily weekly monthly yearly; do
out=$(resolve_one "$p" "$dir") || { printf '%s\t-\tmissing\n' "$p"; rc=3; continue; }
printf '%s\t%s\n' "$p" "$out"
done
exit "$rc" ;;
*) usage ;;
esac