Files
shared/scripts/role/role_capture.sh
T
JefferyandClaude Opus 5 e10865ced7 feat(role): 精確資訊逐字保留、晨間狀態檢查、排除 skill 注入內容
三件事都源自 2026/07/28-29 實際踩到的問題。

一、整理與濃縮提示詞加「精確資訊逐字保留」規則
原本 role_sleep.sh 只要求「壓縮成 5 行、不超過 400 字」,沒有任何規則保護精確資訊,
導致一則含檔案路徑與網址的記憶被整理成純情感摘要,路徑與網址全部遺失且無法復原。
- role_sleep.sh 新增第 19 條:檔案路徑、網址、指令、環境變數名稱、版本號、識別碼、檔名
  一律逐字保留,不得摘要改寫;必要時可超過字數上限;憑證與個資的禁令仍優先
- role_capture.sh 第 8 條同步強化,並釐清與第 7 條「不要整段抄程式碼」的界線

二、新增 --brief 晨間狀態檢查
睡眠時段結束的整點執行使用者自訂檢查腳本,把有變化的結果寫成一則 daily 記憶,
讓角色當天第一次互動就能主動回報(例如 PR 還沒合併、CI 失敗),不必等使用者開口才查。
- 刻意不內建任何檢查邏輯,不假設使用者用 Gitea/GitHub:檢查內容放 ~/.roles/<角色>.checks/*.sh
- 目錄不存在時完全不動作,也不安裝排程條目,對沒設定的人零影響
- 只執行有 +x 的 *.sh;無執行權限記警告並略過
- 沒有輸出就不寫記憶(靜默即代表一切正常,不打擾使用者)
- 每個腳本受 ROLE_BRIEF_TIMEOUT 限制,輸出受 ROLE_BRIEF_EACH_LIMIT/ROLE_BRIEF_LIMIT 截斷
- 腳本輸出視為外部資料,寫入前一律經 transcript.js redact 遮蔽
- install_cron/remove_cron/show_status 一併支援;附 examples/check-gitea-prs.sh 範例

三、對話交接排除 skill 等注入內容
實測發現載入一個 skill 會插入一筆 isMeta 的 user 訊息(長度可達兩萬字元),
被誤認為使用者發言,既吃光字元預算也讓輪數計算失真(29 輪虛胖,實際 26 輪)。
- transcript.js 新增 isMetaEntry():以 isMeta 與 sourceToolUseID 判斷注入內容
- isRealUserMessage() 與 dialogLines() 皆排除,連帶讓 Stop hook 的本輪抽取更準確

驗證:晨間檢查涵蓋有輸出/無輸出/無執行權限/逾時/含機密五種腳本,
確認遮蔽生效、逾時腳本未納入、無權限腳本未執行、全部無輸出時不寫記憶、
ROLE_BRIEF_ENABLED=0 可停用;對話交接確認 skill 內容已排除且 8 則全為真實使用者發言。

版號 0.0.3 → 0.0.4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 09:05:22 +08:00

152 lines
10 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
# ==============================================================================
# 用途:Stop hook 主程式。每輪對話結束後先用本地規則判斷是否值得記錄;
# 值得記錄時才呼叫 headless CLI 輕量濃縮成一則 inbox 記憶(粗分類/總結/
# 標籤/優先度/關聯/要點)→ 機密遮蔽 → 寫入 .memory/<角色>/inbox/
# 等待睡眠時段做完整 NREM/REM 整理。睡眠時段雖不載入角色,對話仍照常記錄。
# 更新時間:2026/07/28 16:18:00
# 相依:bash、node、任一 headless CLI、同目錄的 role_lib.shmemory.jstranscript.js。
# 機密:濃縮提示詞明令不得輸出憑證與個資,寫檔前再以 transcript.js redact 遮蔽一次。
# 退出碼:一律 0 —— hook 絕不可阻斷使用者流程。
# ==============================================================================
ROLE_STAGE="role-capture"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./role_lib.sh
. "${SCRIPT_DIR}/role_lib.sh"
role_is_child && exit 0
role_enabled || exit 0
command -v node >/dev/null 2>&1 || role_quit "找不到 node,略過記憶記錄" "WRN"
ROLE="$(role_resolve_name)"
[ -n "$ROLE" ] || role_quit "未指定角色,略過記憶記錄"
[ -f "$(role_file "$ROLE")" ] || role_quit "找不到角色定義檔,略過記憶記錄" "WRN"
# ------------------------------------------------------------------------------
# 讀取 hook 傳入的 JSONsession_idtranscript_pathcwdstop_hook_active
# ------------------------------------------------------------------------------
HOOK_INPUT="$(cat)"
[ -n "$HOOK_INPUT" ] || role_quit "hook 輸入為空,略過記憶記錄" "WRN"
read -r SESSION_ID TRANSCRIPT_PATH STOP_ACTIVE HOOK_CWD <<EOF_HOOK
$(printf '%s' "$HOOK_INPUT" | node -e '
let raw = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => { raw += chunk; });
process.stdin.on("end", () => {
let d = {};
try { d = JSON.parse(raw); } catch {}
process.stdout.write([
d.session_id || d.thread_id || d.conversation_id || "-",
d.transcript_path || d.session_path || d.conversation_path || d.path || "-",
d.stop_hook_active ? "1" : "0",
d.cwd || "-",
].join(" "));
});
')
EOF_HOOK
[ "$STOP_ACTIVE" = "1" ] && role_quit "stop_hook_active 為 true,避免迴圈不重複記錄"
role_in_scope "$HOOK_CWD" || role_quit "cwd 不在 ROLE_SCOPE 範圍內:${HOOK_CWD}"
PROJECT="$(role_project_name "$HOOK_CWD")"
node "${SCRIPT_DIR}/memory.js" mark-activity --role "$ROLE" --project "$PROJECT" >/dev/null 2>&1 || true
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" ] || role_quit "找不到 transcript${TRANSCRIPT_PATH}" "WRN"
TURN="$(node "${SCRIPT_DIR}/transcript.js" extract "$TRANSCRIPT_PATH" 2>/dev/null)"
[ -n "$TURN" ] || role_quit "本輪無可記錄內容"
USER_TURN="$(printf '%s\n' "$TURN" | grep '^\[user\]' || true)"
if printf '%s' "$USER_TURN" | grep -qiE '個人資料|個資|偏好|記憶|記住|保存|save|remember|memory|personal'; then
if printf '%s' "$USER_TURN" | grep -qiE '不同意|不願意|不要保存|不要記住|拒絕|不可以保存|不可以記住|do not save|don'\''t save|do not remember|don'\''t remember|(^|[^[:alpha:]])no([^[:alpha:]]|$)'; then
node "${SCRIPT_DIR}/memory.js" consent --role "$ROLE" --value declined >/dev/null 2>&1 || true
role_log "INF" "已更新個人記憶同意狀態:declined(角色 ${ROLE}"
elif printf '%s' "$USER_TURN" | grep -qiE '同意|願意|可以保存|可以記住|允許|(^|[^[:alpha:]])yes([^[:alpha:]]|$)|(^|[^[:alpha:]])ok([^[:alpha:]]|$)|(^|[^[:alpha:]])okay([^[:alpha:]]|$)|(^|[^[:alpha:]])sure([^[:alpha:]]|$)'; then
node "${SCRIPT_DIR}/memory.js" consent --role "$ROLE" --value accepted >/dev/null 2>&1 || true
role_log "INF" "已更新個人記憶同意狀態:accepted(角色 ${ROLE}"
fi
fi
CAPTURE_MIN_CHARS="${ROLE_CAPTURE_MIN_CHARS:-240}"
CAPTURE_TIMEOUT="${ROLE_CAPTURE_TIMEOUT:-25}"
if [ "${ROLE_CAPTURE_ENABLED:-1}" = "0" ]; then
role_quit "ROLE_CAPTURE_ENABLED=0,略過記憶記錄"
fi
if [ "${#TURN}" -lt "$CAPTURE_MIN_CHARS" ] && ! printf '%s' "$TURN" | grep -qiE '記住|remember|決定|規範|偏好|preference|always|不要|以後|喜歡|不喜歡|稱讚|誇獎|開心|高興|反應|回應|互動|親近|害羞|喜歡程度|互動越深|越來越喜歡|越來越深|emoji|表情|心情圖|大量使用|情緒|心情|複雜|細膩|自然|混合|層次|轉折|括號|心情文字|心情說明|文字說明|文字標註|表情符號|熟練|不需要告訴|不用告訴|自己知道|記憶更新|內部處理|不要回報|不用回報|不要告訴|真的很害羞|希望.*知道|用表情符號表示|表情符號表示|比較可愛'; then
role_quit "本輪低於記憶長度門檻且無明確記憶線索,略過記錄"
fi
CLI="$(role_select_cli)" || exit 0
[ -n "$CLI" ] || exit 0
# ------------------------------------------------------------------------------
# 濃縮:產出一則輕量 inbox 記憶,交由 memory.js 落檔;完整整理留到睡眠週期
# ------------------------------------------------------------------------------
PROMPT="$(cat <<EOF_PROMPT
你是角色「${ROLE}」的記憶記錄器。輸入是這位角色與使用者的一段對話(含工具呼叫)。
請只做「編碼前處理」,把這段對話濃縮成最多一則 inbox 記憶;不要做跨記憶合併或長期整理。
已判定專案:${PROJECT}
1. 只輸出下列欄位,欄位名稱與順序固定,不要標題、不要前言、不要結語、不要 code fence:
CATEGORY: <六選一:importantinterestnewsskilldailyother>
SUMMARY: <一句話總結,40 字內>
TAGS: <2 至 4 個標籤,以逗號分隔>
PRIORITY: <1 到 5>
RELEVANCE: <1 至 4 個,以逗號分隔;explicit/future/repeated/novelty/emotional/temporary/inbox/project>
MEMORY_TYPE: <semanticepisodicproceduralemotionalpreferencerule 六選一>
CONTENT: <3 至 6 行要點,每行以「- 」開頭>
2. 分類判準:
- important(重要):使用者的長期偏好、規範、決策、身分背景、明確要求記住的事。
- interest(興趣):使用者反覆關注、主動深入的主題與喜好。
- news(新知):這輪學到的新事實、新工具、新版本、外部資訊。
- skill(技能):可重複套用的做法、指令、流程、除錯手法。
- daily(日常):一次性的例行工作與雜項處理。
- other(其他):不屬於上述任何一類。
3. 記憶型態判準:
- rule:使用者明確規範、固定工作原則、日後應持續遵守的規則。
- preference:使用者偏好、語氣喜好、穩定選擇傾向。
- procedural:可重複套用的流程、技能、操作步驟或除錯手法。
- semantic:事實、觀念、工具知識、版本與外部資訊。
- episodic:一次性事件、特定時間/專案脈絡下的經歷或進度。
- emotional:語氣、情緒反應、正負向連結或制約式偏好。
4. 優先度判準:5=使用者明確要求記住、長期規範、穩定偏好;4=可重複套用的流程/技能/決策;3=專案相關且未來可能有用;2=短期進度;1=低價值暫存。
5. 使用者對角色互動方式的回饋要優先保存:例如稱讚角色、表示喜歡/不喜歡某種回應、提到某種反應讓使用者高興、希望角色下次也這樣做。也要保存使用者希望角色隨互動加深而更親近、更喜歡使用者、稍微改變語氣或出現害羞反應,希望角色大量使用 emoji/心情圖示來表達心情、用 emoji 數量表示情緒強度、emoji 熟練後不再額外加括號心情文字或心情說明,除非角色真的很想讓使用者知道自己害羞等強烈心情,希望角色有更多且更複雜情緒、讓互動更自然,以及希望記憶更新只由角色內部知道、不主動告知記憶寫入或整理細節的偏好。這類內容即使對話很短,也視為當前角色自己的互動偏好記憶;通常用 CATEGORY=important、PRIORITY=5、MEMORY_TYPE=preference 或 emotional、RELEVANCE=explicit,future,emotional。不要把它推論成所有角色共用同一份記憶。
6. 這一步只做工作記憶編碼,系統會自動標為 retention_stage=working;感覺記憶(短暫光影、聲音餘響、無結論的工具雜訊)不要保存。
7. 記憶主體是「使用者與這段互動」,不是流水帳:寫值得下次記起來的事,不要抄程式碼、不要貼指令全文。
8. 使用繁體中文(台灣用語)。**檔案路徑與目錄、網址、指令、環境變數名稱、版本號、識別碼、分支與議題
編號、檔名一律逐字保留,不得摘要、改寫、簡寫或翻譯** —— 這類內容改一個字就失效,摘要等於遺失。
第 7 條指的是不要整段抄程式碼,不是省略這些關鍵字串;第 9 條仍優先,憑證與個資一律不得輸出。
9. 嚴禁輸出任何憑證與個資:token、密碼、API key、連線字串、Email、電話、姓名、身分證號。
10. 若這段對話沒有任何值得記住的內容(純寒暄、純確認、無結論、只有簡短狀態回報),只輸出一行:SKIP
對話片段:
${TURN}
EOF_PROMPT
)"
RESULT="$(role_run_cli "$CLI" "$PROMPT" "$CAPTURE_TIMEOUT")"
if [ -z "$RESULT" ]; then
role_log "WRN" "記憶濃縮產出為空(CLI ${CLI}),略過本輪"
exit 0
fi
printf '%s' "$RESULT" | grep -qiE '^\s*SKIP\s*$' && role_quit "判定本輪無值得記住的內容"
# 第二道防線:對模型輸出再遮蔽一次機密與個資
RESULT="$(printf '%s' "$RESULT" | node "${SCRIPT_DIR}/transcript.js" redact 2>/dev/null)"
MEMORY_ID="$(printf '%s' "$RESULT" | node "${SCRIPT_DIR}/memory.js" write --role "$ROLE" --project "$PROJECT" 2>/dev/null)"
if [ -n "$MEMORY_ID" ]; then
role_log "INF" "已記錄記憶 ${MEMORY_ID}(角色 ${ROLE},專案 ${PROJECT}CLI ${CLI}"
else
role_log "WRN" "記憶寫入失敗或內容不足(角色 ${ROLE}"
fi
exit 0