Files
doc/scripts/worklog/worklog.sh
T

191 lines
9.1 KiB
Bash
Executable File
Raw 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 bash
# ==============================================================================
# 用途:工作證明自動記錄(worklog)。由 Claude Code 的 Stop hook 觸發,
# 抽出本輪工作內容 → 呼叫模型濃縮成精簡條目 → 機密遮蔽 →
# 追加到 Gitea wiki 的當週工作紀錄頁。工作內容全程不落地。
# 更新時間:2026/07/27 15:09:00
# 相依:python3、claude CLI、curlwiki 走 Python urllib,不需 curl 亦可)。
# 機密:token 僅由環境變數/本機憑證讀取,不 echo、不寫檔;輸出前套用遮蔽規則。
# 退出碼:一律 0 —— hook 絕不可阻斷使用者的工作流程。
# ==============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STAGE="worklog"
FALLBACK_MODEL="claude-haiku-4-5-20251001"
MODEL_CACHE="${HOME}/.claude/worklog/model"
CACHE_MAX_AGE_DAYS=30
# ------------------------------------------------------------------------------
# 共用函式
# ------------------------------------------------------------------------------
log() {
# 輸出統一格式訊息([時間][階段][等級]: 訊息,一行一則),一律走 stderr
local level="$1" message="$2" stamp
stamp="$(TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M:%S')"
printf '[%s][%s][%s]: %s\n' "$stamp" "$STAGE" "$level" "$message" >&2
if [ -n "${WORKLOG_ERRLOG:-}" ] && [ "$level" = "ERR" ]; then
printf '[%s][%s][%s]: %s\n' "$stamp" "$STAGE" "$level" "$message" >> "${WORKLOG_ERRLOG}" 2>/dev/null
fi
}
die_quiet() {
# 記錄原因後以 0 結束:hook 不得阻斷使用者流程
log "${2:-DBG}" "$1"
exit 0
}
# ------------------------------------------------------------------------------
# 遞迴防護:摘要用的子 claude 行程會再次觸發 Stop hook,必須在此擋掉
# ------------------------------------------------------------------------------
[ -n "${WORKLOG_CHILD:-}" ] && exit 0
# ------------------------------------------------------------------------------
# 啟用檢查:未設定 WORKLOG_* 的環境完全不動作(他人匯入 plugin 零影響)
# ------------------------------------------------------------------------------
[ "${WORKLOG_ENABLED:-}" = "1" ] || exit 0
[ -n "${WORKLOG_HOST:-}" ] || die_quiet "未設定 WORKLOG_HOST,略過記錄" "WRN"
[ -n "${WORKLOG_REPO:-}" ] || die_quiet "未設定 WORKLOG_REPO,略過記錄" "WRN"
command -v python3 >/dev/null 2>&1 || die_quiet "找不到 python3,略過記錄" "WRN"
command -v claude >/dev/null 2>&1 || die_quiet "找不到 claude CLI,略過記錄" "WRN"
# ------------------------------------------------------------------------------
# 讀取 hook 傳入的 JSONsession_idtranscript_pathcwdstop_hook_active
# ------------------------------------------------------------------------------
HOOK_INPUT="$(cat)"
[ -n "$HOOK_INPUT" ] || die_quiet "hook 輸入為空,略過記錄" "WRN"
read -r SESSION_ID TRANSCRIPT_PATH STOP_ACTIVE HOOK_CWD <<EOF_HOOK
$(printf '%s' "$HOOK_INPUT" | python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
except ValueError:
d = {}
print(
d.get("session_id", "-") or "-",
d.get("transcript_path", "-") or "-",
"1" if d.get("stop_hook_active") else "0",
d.get("cwd", "") or "-",
)
')
EOF_HOOK
[ "$STOP_ACTIVE" = "1" ] && die_quiet "stop_hook_active 為 true,避免迴圈不重複記錄"
[ -f "$TRANSCRIPT_PATH" ] || die_quiet "找不到 transcript${TRANSCRIPT_PATH}" "WRN"
# ------------------------------------------------------------------------------
# 記錄範圍:WORKLOG_SCOPE 以冒號分隔的路徑前綴,未設定則全部 session 都記
# ------------------------------------------------------------------------------
if [ -n "${WORKLOG_SCOPE:-}" ]; then
in_scope=0
IFS=':' read -r -a scopes <<< "${WORKLOG_SCOPE}"
for scope in "${scopes[@]}"; do
case "$HOOK_CWD" in "${scope%/}"*) in_scope=1 ;; esac
done
[ "$in_scope" = "1" ] || die_quiet "cwd 不在 WORKLOG_SCOPE 範圍內:${HOOK_CWD}"
fi
# ------------------------------------------------------------------------------
# 專案判定:git remote 的 <owner>/<repo> 優先,其次目錄名
# ------------------------------------------------------------------------------
PROJECT="$(basename "$HOOK_CWD")"
if git -C "$HOOK_CWD" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
origin="$(git -C "$HOOK_CWD" remote get-url origin 2>/dev/null)"
if [ -n "$origin" ]; then
cleaned="${origin%.git}"
cleaned="${cleaned##*://}"
cleaned="${cleaned#*@}"
owner_repo="$(printf '%s' "$cleaned" | awk -F/ 'NF>=2 {print $(NF-1)"/"$NF}')"
[ -n "$owner_repo" ] && PROJECT="$owner_repo"
fi
fi
# ------------------------------------------------------------------------------
# 抽出本輪內容(最後一筆使用者訊息之後),並先做一次機密遮蔽
# ------------------------------------------------------------------------------
TURN="$(python3 "${SCRIPT_DIR}/transcript.py" extract "$TRANSCRIPT_PATH" 2>/dev/null)"
[ -n "$TURN" ] || die_quiet "本輪無可記錄內容"
DURATION="$(python3 "${SCRIPT_DIR}/transcript.py" duration "$TRANSCRIPT_PATH" 2>/dev/null)"
[ -n "$DURATION" ] || DURATION="未判定"
# ------------------------------------------------------------------------------
# 模型決定:WORKLOG_MODEL → 快取檔(/jsc:worklog --tune 產生)→ 保底
# ------------------------------------------------------------------------------
MODEL=""
MODEL_NOTE=""
if [ -n "${WORKLOG_MODEL:-}" ]; then
MODEL="${WORKLOG_MODEL}"
elif [ -f "$MODEL_CACHE" ]; then
if [ -n "$(find "$MODEL_CACHE" -mtime "+${CACHE_MAX_AGE_DAYS}" 2>/dev/null)" ]; then
MODEL="$FALLBACK_MODEL"
MODEL_NOTE=" (model: fallback)"
log "WRN" "模型快取已超過 ${CACHE_MAX_AGE_DAYS} 天,改用保底模型,建議重跑 /jsc:worklog --tune"
else
MODEL="$(grep -m1 -E '^model=' "$MODEL_CACHE" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')"
fi
fi
if [ -z "$MODEL" ]; then
MODEL="$FALLBACK_MODEL"
MODEL_NOTE=" (model: fallback)"
log "WRN" "無模型快取,改用保底模型,建議執行 /jsc:worklog --tune"
fi
# ------------------------------------------------------------------------------
# 濃縮:交給模型產出精簡條目(子行程帶 WORKLOG_CHILD=1 阻斷遞迴)
# ------------------------------------------------------------------------------
PROMPT="$(cat <<EOF_PROMPT
你是工作紀錄濃縮器。輸入是一段 AI 助理與使用者的對話片段(含工具呼叫)。
請濃縮成工作紀錄條目,規則:
已判定專案:${PROJECT}
已估算花費時間:${DURATION}
1. 只輸出 6 個 markdown bullet(以「- 」開頭),不要標題、不要前言、不要結語。
2. 六個 bullet 必須依序使用下列欄位名稱,格式固定為「- 欄位名稱:內容」:
- 專案/任務名稱
- 執行細節與產出
- 花費時間
- 任務狀態
- 遇到的困難
- 解決方式
3. 使用繁體中文(台灣用語),每個 bullet 一行、不超過 90 字,聚焦「做了什麼、動到什麼、結果如何」。
4. 保留關鍵事實:檔案/專案/指令/數量/分支/PR/議題編號;不要抄程式碼、不要貼指令全文。
5. 花費時間優先使用「已估算花費時間」;無法判定時寫「未判定」。
6. 若沒有遇到明確困難,遇到的困難寫「未遇到明確困難」,解決方式寫「不需額外處理」。
7. 嚴禁輸出任何憑證與個資:token、密碼、API key、連線字串、Email、電話、姓名、身分證號。
8. 若這段對話沒有實質工作產出(純閒聊、純提問、僅讀取資訊而未產生結論),只輸出一行:SKIP
EOF_PROMPT
)"
SUMMARY="$(printf '%s' "$TURN" | WORKLOG_CHILD=1 timeout 45 claude -p "$PROMPT" --model "$MODEL" 2>/dev/null)"
if [ -z "$SUMMARY" ]; then
log "WRN" "摘要產出為空(模型 ${MODEL}),略過本輪"
exit 0
fi
printf '%s' "$SUMMARY" | grep -qiE '^\s*SKIP\s*$' && die_quiet "模型判定本輪無實質工作產出"
# 第二道防線:對模型輸出再做一次機密遮蔽
SUMMARY="$(printf '%s' "$SUMMARY" | python3 "${SCRIPT_DIR}/transcript.py" redact 2>/dev/null)"
# 只保留 bullet 行,避免模型帶出多餘敘述
SUMMARY="$(printf '%s\n' "$SUMMARY" | grep -E '^\s*[-*]\s+' | sed -E 's/^\s*[*]/-/' | head -6)"
[ -n "$SUMMARY" ] || die_quiet "摘要不含合法條目,略過本輪" "WRN"
# ------------------------------------------------------------------------------
# 組條目並追加到當週 wiki 頁
# ------------------------------------------------------------------------------
STAMP="$(TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M:%S')"
MARKER="worklog:$(TZ='Asia/Taipei' date +'%Y%m%d%H%M%S')-${SESSION_ID:0:8}"
ENTRY="$(printf '## %s — %s%s <!-- %s -->\n%s\n' "$STAMP" "$PROJECT" "$MODEL_NOTE" "$MARKER" "$SUMMARY")"
export WORKLOG_HOST WORKLOG_REPO
if printf '%s' "$ENTRY" | python3 "${SCRIPT_DIR}/wiki_api.py" append "$MARKER" 2>&1 | grep -q '\[ERR\]'; then
log "ERR" "寫入 wiki 失敗(專案 ${PROJECT}"
else
log "INF" "已記錄工作條目(專案 ${PROJECT},模型 ${MODEL}"
fi
exit 0