feat(worklog): 新增 token 用量統計、支援 Copilot 自動記錄,worklog 腳本改以 Node 實作
- transcript.py/wiki_api.py 等價移植為 transcript.mjs/wiki_api.mjs:extract/duration/redact/probe/pages/show/append 等子命令行為與 Python 版對拍一致;transcript.mjs 額外處理三個 Python→JS 移植地雷(json.dumps 間距、Unicode 碼點切片、Python repr 格式)。
- 新增 tokens 子命令與 turnTokens()/formatTokens():統計本輪 token 用量,Claude Code/Codex/Copilot 三種 transcript 格式皆支援(Copilot 逐輪只有 outputTokens,輸入固定「未判定」)。
- worklog.sh 條目改為七個固定 bullet(新增「token 用量」),hook 輸入解析同時接受 snake_case 與 Copilot 的 camelCase 欄位。
- hooks.json 的腳本定位邏輯補上 ~/.copilot/installed-plugins 搜尋路徑:先前只搜尋 .claude/.codex 的 plugins cache,導致 Copilot 上 hook 事件雖有觸發(agentStop 會被 Copilot 對應到 hooks.json 的 Stop key)卻找不到 worklog.sh。
- SKILL.md/README.md 同步更新腳本檔名、各助理支援範圍(Copilot 改為 ✅ 並附實測說明,Antigravity/OpenCode 補上不支援的具體原因)。
This commit is contained in:
+33
-23
@@ -3,8 +3,8 @@
|
||||
# 用途:工作證明自動記錄(worklog)。由支援 hook 的 CLI 觸發,
|
||||
# 抽出本輪工作內容 → 呼叫已安裝 CLI 濃縮成精簡條目 → 機密遮蔽 →
|
||||
# 追加到 Gitea wiki 的當週工作紀錄頁。工作內容全程不落地。
|
||||
# 更新時間:2026/07/27 17:27:54
|
||||
# 相依:python3、README 定義的任一 headless CLI、curl(wiki 走 Python urllib,不需 curl 亦可)。
|
||||
# 更新時間:2026/08/11 16:51:56
|
||||
# 相依:node(transcript.mjs/wiki_api.mjs)、README 定義的任一 headless CLI。
|
||||
# 機密:token 僅由環境變數/本機憑證讀取,不 echo、不寫檔;輸出前套用遮蔽規則。
|
||||
# 退出碼:一律 0 —— hook 絕不可阻斷使用者的工作流程。
|
||||
# ==============================================================================
|
||||
@@ -48,7 +48,7 @@ die_quiet() {
|
||||
[ -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 node >/dev/null 2>&1 || die_quiet "找不到 node,略過記錄" "WRN"
|
||||
|
||||
select_worklog_cli() {
|
||||
# 依目前 hook/session 環境優先選擇摘要執行器;可用 WORKLOG_CLI 強制指定。
|
||||
@@ -130,29 +130,34 @@ SUMMARY_CLI="$(select_worklog_cli)"
|
||||
[ -n "$SUMMARY_CLI" ] || exit 0
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 讀取 hook 傳入的 JSON(session_id/transcript_path/cwd/stop_hook_active)
|
||||
# 讀取 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 <<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("thread_id") or d.get("conversation_id") or "-",
|
||||
d.get("transcript_path") or d.get("session_path") or d.get("conversation_path") or d.get("path") or "-",
|
||||
"1" if d.get("stop_hook_active") else "0",
|
||||
d.get("cwd", "") or "-",
|
||||
)
|
||||
$(printf '%s' "$HOOK_INPUT" | node -e '
|
||||
let raw = "";
|
||||
process.stdin.on("data", (c) => { 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="-"
|
||||
@@ -190,10 +195,12 @@ fi
|
||||
# ------------------------------------------------------------------------------
|
||||
# 抽出本輪內容(最後一筆使用者訊息之後),並先做一次機密遮蔽
|
||||
# ------------------------------------------------------------------------------
|
||||
TURN="$(python3 "${SCRIPT_DIR}/transcript.py" extract "$TRANSCRIPT_PATH" 2>/dev/null)"
|
||||
TURN="$(node "${SCRIPT_DIR}/transcript.mjs" extract "$TRANSCRIPT_PATH" 2>/dev/null)"
|
||||
[ -n "$TURN" ] || die_quiet "本輪無可記錄內容"
|
||||
DURATION="$(python3 "${SCRIPT_DIR}/transcript.py" duration "$TRANSCRIPT_PATH" 2>/dev/null)"
|
||||
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 使用各自預設模型
|
||||
@@ -232,21 +239,24 @@ PROMPT="$(cat <<EOF_PROMPT
|
||||
|
||||
已判定專案:${PROJECT}
|
||||
已估算花費時間:${DURATION}
|
||||
已統計 token 用量:${TOKENS}
|
||||
|
||||
1. 只輸出 6 個 markdown bullet(以「- 」開頭),不要標題、不要前言、不要結語。
|
||||
2. 六個 bullet 必須依序使用下列欄位名稱,格式固定為「- 欄位名稱:內容」:
|
||||
1. 只輸出 7 個 markdown bullet(以「- 」開頭),不要標題、不要前言、不要結語。
|
||||
2. 七個 bullet 必須依序使用下列欄位名稱,格式固定為「- 欄位名稱:內容」:
|
||||
- 專案/任務名稱
|
||||
- 執行細節與產出
|
||||
- 花費時間
|
||||
- 任務狀態
|
||||
- 遇到的困難
|
||||
- 解決方式
|
||||
- token 用量
|
||||
3. 使用繁體中文(台灣用語),每個 bullet 一行、不超過 90 字,聚焦「做了什麼、動到什麼、結果如何」。
|
||||
4. 保留關鍵事實:檔案/專案/指令/數量/分支/PR/議題編號;不要抄程式碼、不要貼指令全文。
|
||||
5. 花費時間優先使用「已估算花費時間」;無法判定時寫「未判定」。
|
||||
6. 若沒有遇到明確困難,遇到的困難寫「未遇到明確困難」,解決方式寫「不需額外處理」。
|
||||
7. 嚴禁輸出任何憑證與個資:token、密碼、API key、連線字串、Email、電話、姓名、身分證號。
|
||||
8. 若這段對話沒有實質工作產出(純閒聊、純提問、僅讀取資訊而未產生結論),只輸出一行:SKIP
|
||||
9. token 用量一律照抄「已統計 token 用量」,不得自行推算或估計;無法判定時寫「未判定」。
|
||||
|
||||
對話片段:
|
||||
${TURN}
|
||||
@@ -261,9 +271,9 @@ fi
|
||||
printf '%s' "$SUMMARY" | grep -qiE '^\s*SKIP\s*$' && die_quiet "模型判定本輪無實質工作產出"
|
||||
|
||||
# 第二道防線:對模型輸出再做一次機密遮蔽
|
||||
SUMMARY="$(printf '%s' "$SUMMARY" | python3 "${SCRIPT_DIR}/transcript.py" redact 2>/dev/null)"
|
||||
SUMMARY="$(printf '%s' "$SUMMARY" | node "${SCRIPT_DIR}/transcript.mjs" redact 2>/dev/null)"
|
||||
# 只保留 bullet 行,避免模型帶出多餘敘述
|
||||
SUMMARY="$(printf '%s\n' "$SUMMARY" | grep -E '^\s*[-*]\s+' | sed -E 's/^\s*[*]/-/' | head -6)"
|
||||
SUMMARY="$(printf '%s\n' "$SUMMARY" | grep -E '^\s*[-*]\s+' | sed -E 's/^\s*[*]/-/' | head -7)"
|
||||
[ -n "$SUMMARY" ] || die_quiet "摘要不含合法條目,略過本輪" "WRN"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -275,7 +285,7 @@ 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
|
||||
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})"
|
||||
|
||||
Reference in New Issue
Block a user