From 3193cce24908186e724c1bec7bf164e3ef1ff115 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 17 Aug 2026 15:22:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(worklog):=20Stop=20hook=20=E8=A3=9C?= =?UTF-8?q?=E4=B8=8A=20--use-system-ca=EF=BC=8C=E5=88=AA=E9=99=A4=E5=B7=B2?= =?UTF-8?q?=E6=94=B9=E5=AF=AB=E5=AE=8C=E6=88=90=E7=9A=84=20worklog.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 實測 worklog.mjs 全流程(含真的呼叫 claude -p 濃縮並寫入 Gitea wiki 週頁) 時發現 hooks.json 的 Stop command 漏加 --use-system-ca:本環境的 TLS 攔截代理會讓 Node fetch 預設信任鏈驗證失敗(HTTP 0),造成 wiki_api.mjs 誤判兩個 token 來源都失效而寫入失敗;shared/code/persona 的 version-guard hook 都有這個旗標,只有 doc 的 worklog Stop hook 漏掉。 加上後於既有 knowledges/Plan wiki 建立測試週頁重測通過(七欄格式正確、 兩次呼叫的 marker 各自附加不互相覆蓋),驗證後已刪除該測試頁。 worklog.sh 已確認可被 worklog.mjs 完全取代,直接刪除、不保留 bash fallback;grep -rn 'worklog.sh' doc/ 回傳零筆。 Co-Authored-By: Claude Sonnet 5 --- hooks/hooks.json | 2 +- scripts/worklog/worklog.sh | 294 ------------------------------------- 2 files changed, 1 insertion(+), 295 deletions(-) delete mode 100755 scripts/worklog/worklog.sh diff --git a/hooks/hooks.json b/hooks/hooks.json index c6a2507..5ab9a37 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -18,7 +18,7 @@ "hooks": [ { "type": "command", - "command": "rel='scripts/worklog/worklog.mjs'; own='doc'; plug='jsc-doc'; root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ] && [ -f \"$root/$rel\" ]; then exec node \"$root/$rel\"; fi; for base in \"$HOME/.claude/plugins/cache\" \"$HOME/.codex/plugins/cache\" \"$HOME/.copilot/installed-plugins\"; do for dir in \"$base/$own/$plug\" \"$base\"; do s=$(find \"$dir\" -path \"*/$plug/*/$rel\" -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$s\" ]; then exec node \"$s\"; fi; done; done; exit 0", + "command": "rel='scripts/worklog/worklog.mjs'; own='doc'; plug='jsc-doc'; root=\"${CLAUDE_PLUGIN_ROOT:-}\"; if [ -n \"$root\" ] && [ -f \"$root/$rel\" ]; then exec node --use-system-ca \"$root/$rel\"; fi; for base in \"$HOME/.claude/plugins/cache\" \"$HOME/.codex/plugins/cache\" \"$HOME/.copilot/installed-plugins\"; do for dir in \"$base/$own/$plug\" \"$base\"; do s=$(find \"$dir\" -path \"*/$plug/*/$rel\" -type f 2>/dev/null | sort -V | tail -n 1); if [ -n \"$s\" ]; then exec node --use-system-ca \"$s\"; fi; done; done; exit 0", "timeout": 60 } ] diff --git a/scripts/worklog/worklog.sh b/scripts/worklog/worklog.sh deleted file mode 100755 index e24a06f..0000000 --- a/scripts/worklog/worklog.sh +++ /dev/null @@ -1,294 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================== -# 用途:工作證明自動記錄(worklog)。由支援 hook 的 CLI 觸發, -# 抽出本輪工作內容 → 呼叫已安裝 CLI 濃縮成精簡條目 → 機密遮蔽 → -# 追加到 Gitea wiki 的當週工作紀錄頁。工作內容全程不落地。 -# 更新時間:2026/08/11 16:51:56 -# 相依:node(transcript.mjs/wiki_api.mjs)、README 定義的任一 headless CLI。 -# 機密:token 僅由環境變數/本機憑證讀取,不 echo、不寫檔;輸出前套用遮蔽規則。 -# 退出碼:一律 0 —— hook 絕不可阻斷使用者的工作流程。 -# ============================================================================== - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -STAGE="worklog" -SUPPORTED_CLIS="claude codex agy opencode copilot" -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 -} - -# ------------------------------------------------------------------------------ -# 遞迴防護:摘要用的子 CLI 行程可能再次觸發 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 node >/dev/null 2>&1 || die_quiet "找不到 node,略過記錄" "WRN" - -select_worklog_cli() { - # 依目前 hook/session 環境優先選擇摘要執行器;可用 WORKLOG_CLI 強制指定。 - local requested="${WORKLOG_CLI:-auto}" cli current_cli - if [ "$requested" != "auto" ]; then - case " ${SUPPORTED_CLIS} " in - *" ${requested} "*) ;; - *) die_quiet "WORKLOG_CLI 不支援:${requested}(可用:auto ${SUPPORTED_CLIS})" "WRN" ;; - esac - command -v "$requested" >/dev/null 2>&1 || die_quiet "找不到 ${requested} CLI,略過記錄" "WRN" - printf '%s' "$requested" - return 0 - fi - current_cli="$(detect_current_cli)" - if [ -n "$current_cli" ]; then - if command -v "$current_cli" >/dev/null 2>&1; then - printf '%s' "$current_cli" - return 0 - fi - log "WRN" "目前環境判定為 ${current_cli},但找不到 ${current_cli} CLI,改用可用摘要 CLI" - fi - for cli in $SUPPORTED_CLIS; do - if command -v "$cli" >/dev/null 2>&1; then - printf '%s' "$cli" - return 0 - fi - done - die_quiet "找不到可用摘要 CLI(需要其一:${SUPPORTED_CLIS})" "WRN" -} - -detect_current_cli() { - # 判斷實際觸發本輪 hook 的助理環境,避免 auto 因 PATH 順序誤顯其他 CLI。 - if [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_CI:-}" ] || [ -n "${CODEX_MANAGED_PACKAGE_ROOT:-}" ]; then - printf 'codex' - return 0 - fi - if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || [ -n "${CLAUDE_CODE_SSE_PORT:-}" ]; then - printf 'claude' - return 0 - fi - if [ -n "${AGY_SESSION_ID:-}" ] || [ -n "${AGY_WORKSPACE_ID:-}" ]; then - printf 'agy' - return 0 - fi - if [ -n "${OPENCODE_SESSION_ID:-}" ] || [ -n "${OPENCODE_CONFIG:-}" ]; then - printf 'opencode' - return 0 - fi - if [ -n "${COPILOT_AGENT_ID:-}" ] || [ -n "${GITHUB_COPILOT_TOKEN:-}" ]; then - printf 'copilot' - return 0 - fi - return 0 -} - -run_summary_cli() { - # 各 CLI 依 README 的 headless 指令呼叫;不把工作內容寫入檔案。 - local cli="$1" prompt="$2" model="$3" - case "$cli" in - claude) - WORKLOG_CHILD=1 timeout 45 claude -p "$prompt" --model "$model" 2>/dev/null - ;; - codex) - WORKLOG_CHILD=1 timeout 45 codex exec "$prompt" 2>/dev/null - ;; - agy) - WORKLOG_CHILD=1 timeout 45 agy -p "$prompt" 2>/dev/null - ;; - opencode) - WORKLOG_CHILD=1 timeout 45 opencode run "$prompt" 2>/dev/null - ;; - copilot) - WORKLOG_CHILD=1 timeout 45 copilot -p "$prompt" 2>/dev/null - ;; - esac -} - -SUMMARY_CLI="$(select_worklog_cli)" -[ -n "$SUMMARY_CLI" ] || exit 0 - -# ------------------------------------------------------------------------------ -# 讀取 hook 傳入的 JSON。欄位名稱大小寫依助理而異:Claude Code/Codex 用 -# snake_case(session_id/transcript_path),Copilot 的 agentStop 事件用 -# camelCase(sessionId/transcriptPath,實測見 H1-1),只有 stop_hook_active -# 剛好三家都是 snake_case。兩種寫法都要接,缺一個 Copilot 就完全取不到值。 -# ------------------------------------------------------------------------------ -HOOK_INPUT="$(cat)" -[ -n "$HOOK_INPUT" ] || die_quiet "hook 輸入為空,略過記錄" "WRN" - -read -r SESSION_ID TRANSCRIPT_PATH STOP_ACTIVE HOOK_CWD < { raw += c; }); -process.stdin.on("end", () => { - let d = {}; - try { d = JSON.parse(raw); } catch { d = {}; } - const sessionId = d.session_id || d.sessionId || d.thread_id || d.conversation_id || "-"; - const transcriptPath = d.transcript_path || d.transcriptPath || d.session_path || d.conversation_path || d.path || "-"; - const stopActive = (d.stop_hook_active || d.stopHookActive) ? "1" : "0"; - const cwd = d.cwd || "-"; - console.log(sessionId, transcriptPath, stopActive, cwd); -}); -') -EOF_HOOK - -[ "$STOP_ACTIVE" = "1" ] && die_quiet "stop_hook_active 為 true,避免迴圈不重複記錄" - -# Codex 的 hook 只給 thread id、不給 transcript 路徑,需要自己找檔案; -# Copilot 的 agentStop 事件已直接帶 transcriptPath(見上方解析),不需要這段 fallback。 -if [ ! -f "$TRANSCRIPT_PATH" ] && [ -n "${CODEX_THREAD_ID:-}" ]; then - TRANSCRIPT_PATH="$(find "${HOME}/.codex/sessions" -type f -name "*${CODEX_THREAD_ID}.jsonl" -print -quit 2>/dev/null)" - [ -n "$TRANSCRIPT_PATH" ] || TRANSCRIPT_PATH="-" -fi - -[ -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 的 / 優先,其次目錄名 -# ------------------------------------------------------------------------------ -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="$(node "${SCRIPT_DIR}/transcript.mjs" extract "$TRANSCRIPT_PATH" 2>/dev/null)" -[ -n "$TURN" ] || die_quiet "本輪無可記錄內容" -DURATION="$(node "${SCRIPT_DIR}/transcript.mjs" duration "$TRANSCRIPT_PATH" 2>/dev/null)" -[ -n "$DURATION" ] || DURATION="未判定" -TOKENS="$(node "${SCRIPT_DIR}/transcript.mjs" tokens "$TRANSCRIPT_PATH" 2>/dev/null)" -[ -n "$TOKENS" ] || TOKENS="未判定" - -# ------------------------------------------------------------------------------ -# 模型決定:只有 claude CLI 使用 WORKLOG_MODEL/快取檔;其他 CLI 使用各自預設模型 -# ------------------------------------------------------------------------------ -MODEL="" -MODEL_NOTE="" -if [ "$SUMMARY_CLI" = "claude" ]; then - 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=" (cli: claude, model: fallback)" - log "WRN" "模型快取已超過 ${CACHE_MAX_AGE_DAYS} 天,改用保底模型,建議重跑 /jsc-doc: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=" (cli: claude, model: fallback)" - log "WRN" "無模型快取,改用保底模型,建議執行 /jsc-doc:worklog --tune" - elif [ -z "$MODEL_NOTE" ]; then - MODEL_NOTE=" (cli: claude)" - fi -else - MODEL_NOTE=" (cli: ${SUMMARY_CLI})" -fi - -# ------------------------------------------------------------------------------ -# 濃縮:交給選定 CLI 產出精簡條目(子行程帶 WORKLOG_CHILD=1 阻斷遞迴) -# ------------------------------------------------------------------------------ -PROMPT="$(cat </dev/null)" -# 只保留 bullet 行,避免模型帶出多餘敘述 -SUMMARY="$(printf '%s\n' "$SUMMARY" | grep -E '^\s*[-*]\s+' | sed -E 's/^\s*[*]/-/' | head -7)" -[ -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 \n%s\n' "$STAMP" "$PROJECT" "$MODEL_NOTE" "$MARKER" "$SUMMARY")" - -export WORKLOG_HOST WORKLOG_REPO -if printf '%s' "$ENTRY" | node "${SCRIPT_DIR}/wiki_api.mjs" append "$MARKER" 2>&1 | grep -q '\[ERR\]'; then - log "ERR" "寫入 wiki 失敗(專案 ${PROJECT})" -else - log "INF" "已記錄工作條目(專案 ${PROJECT},CLI ${SUMMARY_CLI})" -fi - -exit 0