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>
This commit is contained in:
+143
-7
@@ -20,6 +20,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
CRON_MARKER="# jsc-role-sleep"
|
||||
NAP_CRON_MARKER="# jsc-role-nap"
|
||||
BRIEF_CRON_MARKER="# jsc-role-brief"
|
||||
SLEEP_TIMEOUT="${ROLE_SLEEP_TIMEOUT:-180}"
|
||||
SLEEP_OUTPUT_LIMIT="${ROLE_SLEEP_OUTPUT_LIMIT:-8000}"
|
||||
|
||||
@@ -32,6 +33,7 @@ usage() {
|
||||
--nap 小睡觸發:CLI 閒置一段時間且 inbox 達門檻時整理記憶
|
||||
--catchup 補跑:cron 未執行時,由 SessionStart hook 於背景呼叫
|
||||
--force 立即整理一次(忽略時段與 AI 運行檢查)
|
||||
--brief 晨間狀態檢查:執行使用者自訂檢查腳本並寫成一則記憶
|
||||
--export <路徑> 匯出目前角色定義、資產與記憶為 .tar.gz
|
||||
--export <角色 ID> <路徑>
|
||||
--install-cron 安裝/更新睡眠排程(每小時檢查一次)
|
||||
@@ -151,9 +153,15 @@ sleep_cycle() {
|
||||
13. 感覺記憶(短暫光影、聲音餘響、無結論的工具雜訊)一律 drop;不要保存到長期記憶。
|
||||
14. **每一則 INBOX 的 id 都必須出現在某一筆的 from 中**,沒被提及的會留到下個睡眠週期重做。
|
||||
15. content 壓縮成 5 行以內要點(每行以「- 」開頭),總長不超過 400 字,去除重複敘述與流水帳。
|
||||
但精確資訊不受此壓縮限制,見第 19 條。
|
||||
16. summary 一句話 40 字內;tags 2 至 4 個。全部使用繁體中文(台灣用語)。
|
||||
17. sleepDigest 總結本次新增、合併、丟棄、抽象化或建立關聯的重點,80 字內。
|
||||
18. 嚴禁輸出任何憑證與個資:token、密碼、API key、連線字串、Email、電話、姓名、身分證號。
|
||||
19. **精確資訊一律逐字保留,不得摘要、改寫、簡寫、翻譯或省略**:檔案路徑與目錄、網址、指令與參數、
|
||||
環境變數名稱、版本號、識別碼、檔名。這類內容改一個字就失效,摘要等於直接遺失。
|
||||
若保留後超過第 15 條字數上限,以保留精確資訊為優先,寧可多一兩行。
|
||||
第 18 條仍然優先:憑證與個資即使屬於精確資訊也一律不得輸出。
|
||||
(實測教訓:曾有一則含檔案路徑與網址的記憶被整理成純情感摘要,路徑與網址全部遺失且無法復原。)
|
||||
|
||||
素材:
|
||||
${material}
|
||||
@@ -246,6 +254,109 @@ nap_cron_line() {
|
||||
"$(cron_quote "$(sleep_log_path)")" "$NAP_CRON_MARKER"
|
||||
}
|
||||
|
||||
brief_enabled() {
|
||||
case "${ROLE_BRIEF_ENABLED:-1}" in
|
||||
0|false|no|off) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
brief_timeout() { printf '%s' "${ROLE_BRIEF_TIMEOUT:-30}"; }
|
||||
brief_limit() { printf '%s' "${ROLE_BRIEF_LIMIT:-2000}"; }
|
||||
brief_each_limit() { printf '%s' "${ROLE_BRIEF_EACH_LIMIT:-600}"; }
|
||||
|
||||
checks_dir() {
|
||||
# 使用者自訂的檢查腳本目錄;刻意不預設任何內容,沒有目錄就等於停用
|
||||
printf '%s/%s.checks' "$(role_home)" "$ROLE"
|
||||
}
|
||||
|
||||
brief_hour() {
|
||||
# 在睡眠時段結束的整點執行,讓使用者起床前狀態已就緒
|
||||
local end hour
|
||||
end="$(role_sleep_end)"
|
||||
hour="${end%%:*}"
|
||||
case "$hour" in
|
||||
''|*[!0-9]*) printf '6' ;;
|
||||
*) printf '%s' "$((10#$hour))" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
brief_cron_line() {
|
||||
# 組出晨間狀態檢查條目:每日睡眠結束時執行一次
|
||||
printf '0 %s * * * %s %s --brief >> %s 2>&1 %s\n' \
|
||||
"$(brief_hour)" "$(cron_env_prefix)" "$(cron_quote "${SCRIPT_DIR}/role_sleep.sh")" \
|
||||
"$(cron_quote "$(sleep_log_path)")" "$BRIEF_CRON_MARKER"
|
||||
}
|
||||
|
||||
run_brief() {
|
||||
# 晨間狀態檢查:執行使用者自訂腳本,把有變化的結果寫成一則記憶。
|
||||
#
|
||||
# 設計取捨:本 skill 不內建任何檢查邏輯(不假設使用者用 Gitea、GitHub 或任何服務),
|
||||
# 改由使用者自行在 <角色 ID>.checks/ 放可執行腳本。沒有該目錄時完全不動作,對沒設定的人零影響。
|
||||
# 腳本輸出視為外部資料:逐一限制長度、加 timeout,寫入前一律走 redact 遮蔽憑證與個資。
|
||||
local dir timeout_s each_limit total_limit collected="" ran=0 skipped=0 reported=0
|
||||
dir="$(checks_dir)"
|
||||
if [ ! -d "$dir" ]; then
|
||||
role_log "DBG" "沒有檢查腳本目錄(${dir}),略過晨間狀態檢查"
|
||||
return 0
|
||||
fi
|
||||
timeout_s="$(brief_timeout)"
|
||||
each_limit="$(brief_each_limit)"
|
||||
total_limit="$(brief_limit)"
|
||||
|
||||
local script name result
|
||||
for script in "$dir"/*.sh; do
|
||||
[ -f "$script" ] || continue
|
||||
name="${script##*/}"
|
||||
if [ ! -x "$script" ]; then
|
||||
role_log "WRN" "檢查腳本沒有執行權限,略過:${name}(chmod +x 後生效)"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
result="$(timeout "$timeout_s" "$script" 2>&1 | head -c "$each_limit")"
|
||||
else
|
||||
result="$("$script" 2>&1 | head -c "$each_limit")"
|
||||
fi
|
||||
ran=$((ran + 1))
|
||||
result="$(printf '%s' "$result" | sed '/^[[:space:]]*$/d')"
|
||||
if [ -n "$result" ]; then
|
||||
reported=$((reported + 1))
|
||||
if [ -n "$collected" ]; then
|
||||
collected="$(printf '%s\n- 【%s】\n%s' "$collected" "$name" "$result")"
|
||||
else
|
||||
collected="$(printf -- '- 【%s】\n%s' "$name" "$result")"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ran" = 0 ]; then
|
||||
role_log "DBG" "檢查腳本目錄沒有可執行腳本(略過 ${skipped} 個),略過晨間狀態檢查"
|
||||
return 0
|
||||
fi
|
||||
if [ -z "$collected" ]; then
|
||||
role_log "INF" "晨間狀態檢查完成:執行 ${ran} 個腳本,沒有需要回報的變化"
|
||||
return 0
|
||||
fi
|
||||
|
||||
collected="$(printf '%s' "$collected" | head -c "$total_limit" | node "${SCRIPT_DIR}/transcript.js" redact 2>/dev/null)"
|
||||
[ -n "$collected" ] || return 0
|
||||
|
||||
local today
|
||||
today="$(TZ='Asia/Taipei' date +'%Y/%m/%d')"
|
||||
{
|
||||
printf 'CATEGORY: daily\n'
|
||||
printf 'SUMMARY: %s 晨間狀態檢查:%s 個腳本有回報(共執行 %s 個)\n' "$today" "$reported" "$ran"
|
||||
printf 'TAGS: 晨間檢查,狀態回報,待處理\n'
|
||||
printf 'CONTENT:\n'
|
||||
printf -- '- 由 %s 的檢查腳本於睡眠時段結束時自動收集,供本日第一次互動時主動回報使用者\n' "$dir"
|
||||
printf '%s\n' "$collected"
|
||||
} | node "${SCRIPT_DIR}/memory.js" write --role "$ROLE" >/dev/null 2>&1
|
||||
|
||||
role_log "INF" "晨間狀態檢查完成:執行 ${ran} 個腳本、${reported} 個有回報,已寫入一則記憶"
|
||||
return 0
|
||||
}
|
||||
|
||||
cron_hours() {
|
||||
# 依睡眠時段換算 cron 小時欄位(每小時檢查一次,讓 AI 運行中的情況能在下個小時重試)
|
||||
local start end hour hours=""
|
||||
@@ -271,11 +382,14 @@ install_cron() {
|
||||
command -v crontab >/dev/null 2>&1 || { role_log "ERR" "找不到 crontab,無法安裝排程"; return 1; }
|
||||
mkdir -p "$(role_home)" 2>/dev/null
|
||||
local current new
|
||||
current="$(crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER" | grep -v -F "$NAP_CRON_MARKER")"
|
||||
current="$(crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER" | grep -v -F "$NAP_CRON_MARKER" | grep -v -F "$BRIEF_CRON_MARKER")"
|
||||
new="$(printf '%s\n%s' "$current" "$(cron_line)" | sed '/^$/d')"
|
||||
if nap_enabled; then
|
||||
new="$(printf '%s\n%s\n%s' "$current" "$(cron_line)" "$(nap_cron_line)" | sed '/^$/d')"
|
||||
else
|
||||
new="$(printf '%s\n%s' "$current" "$(cron_line)" | sed '/^$/d')"
|
||||
new="$(printf '%s\n%s' "$new" "$(nap_cron_line)")"
|
||||
fi
|
||||
# 晨間狀態檢查只在使用者建立了檢查腳本目錄時才排程,避免對沒設定的人留下無用條目
|
||||
if brief_enabled && [ -d "$(checks_dir)" ]; then
|
||||
new="$(printf '%s\n%s' "$new" "$(brief_cron_line)")"
|
||||
fi
|
||||
printf '%s\n' "$new" | crontab - || { role_log "ERR" "寫入 crontab 失敗"; return 1; }
|
||||
role_log "INF" "已安裝睡眠排程:每日 $(cron_hours) 時整點檢查(角色 ${ROLE},時段 $(role_sleep_start)–$(role_sleep_end))"
|
||||
@@ -284,6 +398,11 @@ install_cron() {
|
||||
else
|
||||
role_log "INF" "小睡排程已停用(ROLE_NAP_ENABLED=${ROLE_NAP_ENABLED:-1})"
|
||||
fi
|
||||
if brief_enabled && [ -d "$(checks_dir)" ]; then
|
||||
role_log "INF" "已安裝晨間狀態檢查排程:每日 $(brief_hour) 時執行 $(checks_dir) 內的檢查腳本"
|
||||
else
|
||||
role_log "DBG" "未安裝晨間狀態檢查排程(需建立 $(checks_dir) 並放入可執行的 *.sh)"
|
||||
fi
|
||||
role_log "INF" "排程輸出:$(sleep_log_path)"
|
||||
if ! pgrep -x cron >/dev/null 2>&1 && ! pgrep -x crond >/dev/null 2>&1; then
|
||||
role_log "WRN" "系統 cron 服務未執行(WSL 常見),排程不會觸發;SessionStart 的背景補跑仍會運作"
|
||||
@@ -294,16 +413,22 @@ install_cron() {
|
||||
remove_cron() {
|
||||
# 移除本 skill 安裝的排程條目
|
||||
command -v crontab >/dev/null 2>&1 || { role_log "ERR" "找不到 crontab"; return 1; }
|
||||
crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER" | grep -v -F "$NAP_CRON_MARKER" | crontab -
|
||||
role_log "INF" "已移除睡眠排程"
|
||||
crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER" | grep -v -F "$NAP_CRON_MARKER" | grep -v -F "$BRIEF_CRON_MARKER" | crontab -
|
||||
role_log "INF" "已移除睡眠、小睡與晨間狀態檢查排程"
|
||||
return 0
|
||||
}
|
||||
|
||||
show_status() {
|
||||
# 以表格輸出目前角色與記憶狀態(供 skill 的 --status 使用)
|
||||
local cron_state="未安裝" nap_state="未安裝" cron_service="未執行" window="否"
|
||||
local cron_state="未安裝" nap_state="未安裝" brief_state="未安裝" cron_service="未執行" window="否" checks_state
|
||||
crontab -l 2>/dev/null | grep -qF "$CRON_MARKER" && cron_state="已安裝"
|
||||
crontab -l 2>/dev/null | grep -qF "$NAP_CRON_MARKER" && nap_state="已安裝"
|
||||
crontab -l 2>/dev/null | grep -qF "$BRIEF_CRON_MARKER" && brief_state="已安裝"
|
||||
if [ -d "$(checks_dir)" ]; then
|
||||
checks_state="$(checks_dir)($(find "$(checks_dir)" -maxdepth 1 -name '*.sh' 2>/dev/null | wc -l) 個 .sh)"
|
||||
else
|
||||
checks_state="未建立($(checks_dir))"
|
||||
fi
|
||||
{ pgrep -x cron >/dev/null 2>&1 || pgrep -x crond >/dev/null 2>&1; } && cron_service="執行中"
|
||||
role_in_sleep_window && window="是"
|
||||
printf '| 項目 | 值 |\n| --- | --- |\n'
|
||||
@@ -313,6 +438,8 @@ show_status() {
|
||||
printf '| 目前是否睡眠中 | %s |\n' "$window"
|
||||
printf '| cron 排程 | %s |\n' "$cron_state"
|
||||
printf '| 小睡排程 | %s |\n' "$nap_state"
|
||||
printf '| 晨間檢查排程 | %s |\n' "$brief_state"
|
||||
printf '| 檢查腳本目錄 | %s |\n' "$checks_state"
|
||||
printf '| 小睡啟用 | %s |\n' "$(nap_enabled && printf '是' || printf '否')"
|
||||
printf '| 小睡條件 | 閒置 ≥ %s 分鐘,待整理 ≥ %s 則,每 %s 分鐘檢查 |\n' "$(nap_idle_minutes)" "$(nap_min_inbox)" "$(nap_interval_minutes)"
|
||||
printf '| cron 服務 | %s |\n' "$cron_service"
|
||||
@@ -432,6 +559,15 @@ case "$MODE" in
|
||||
require_role
|
||||
sleep_cycle "手動"
|
||||
;;
|
||||
--brief)
|
||||
role_enabled || exit 0
|
||||
require_role
|
||||
if ! brief_enabled; then
|
||||
role_log "DBG" "晨間狀態檢查已停用(ROLE_BRIEF_ENABLED=${ROLE_BRIEF_ENABLED:-1}),略過"
|
||||
exit 0
|
||||
fi
|
||||
run_brief
|
||||
;;
|
||||
--export)
|
||||
if [ -n "${3:-}" ]; then
|
||||
ROLE="$2"
|
||||
|
||||
Reference in New Issue
Block a user