#!/usr/bin/env bash # ============================================================================== # 用途:角色的睡眠與記憶整理。由 cron 於睡眠時段每小時觸發(--run), # 先檢查是否有 AI 正在運行,沒有才進入睡眠並整理記憶: # 分類(重要/興趣/新知/技能/日常/其他)→ 去重合併 → 設標籤與一句話 # 總結 → 壓縮內容歸檔 → 日常與其他依使用頻率遺忘。 # 另提供 --catchup(cron 未執行時的補跑)、--force(手動立即整理)、 # --install-cron/--remove-cron(排程安裝與移除)、--status(狀態)。 # 更新時間:2026/07/28 00:00:00 # 相依:bash、python3、任一 headless CLI、crontab(僅排程安裝需要)、 # 同目錄的 role_lib.sh 與 memory.py。 # 退出碼:0 成功或無事可做;1 參數錯誤或整理失敗(cron 觸發時不影響使用者)。 # ============================================================================== ROLE_STAGE="role-sleep" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=./role_lib.sh . "${SCRIPT_DIR}/role_lib.sh" CRON_MARKER="# jsc-role-sleep" SLEEP_TIMEOUT="${ROLE_SLEEP_TIMEOUT:-180}" usage() { # 印出用法 cat <<'EOF_USAGE' 用法:role_sleep.sh <模式> --run cron 觸發:在睡眠時段內且無 AI 運行時整理記憶 --catchup 補跑:cron 未執行時,由 SessionStart hook 於背景呼叫 --force 立即整理一次(忽略時段與 AI 運行檢查) --install-cron 安裝/更新睡眠排程(每小時檢查一次) --remove-cron 移除睡眠排程 --status 顯示角色、睡眠時段、排程與記憶統計 EOF_USAGE } require_role() { # 解析角色並確認定義檔存在,取不到時中止 ROLE="$(role_resolve_name)" [ -n "$ROLE" ] || { role_log "WRN" "未指定角色(ROLE_NAME 與 .active 皆無)"; exit 1; } [ -f "$(role_file "$ROLE")" ] || { role_log "WRN" "找不到角色定義檔:$(role_file "$ROLE")"; exit 1; } } # ------------------------------------------------------------------------------ # 整理主流程 # ------------------------------------------------------------------------------ sleep_cycle() { # 執行一次完整記憶整理:收集素材 → 模型分類去重 → 落檔歸檔 → 遺忘 local reason="$1" cli material prompt result applied forgotten command -v python3 >/dev/null 2>&1 || { role_log "ERR" "找不到 python3,無法整理記憶"; return 1; } if ! role_lock_acquire "$ROLE"; then role_log "WRN" "另一個整理程序正在執行,本次略過(角色 ${ROLE})" return 0 fi trap 'role_lock_release "$ROLE"' EXIT material="$(python3 "${SCRIPT_DIR}/memory.py" collect --role "$ROLE" 2>/dev/null)" if [ -z "$material" ]; then role_log "INF" "沒有待整理記憶(角色 ${ROLE},觸發:${reason})" python3 "${SCRIPT_DIR}/memory.py" mark-sleep --role "$ROLE" >/dev/null 2>&1 forgotten="$(python3 "${SCRIPT_DIR}/memory.py" forget --role "$ROLE" 2>/dev/null)" role_log "INF" "遺忘檢查:${forgotten}" role_lock_release "$ROLE" trap - EXIT return 0 fi cli="$(role_select_cli)" || { role_lock_release "$ROLE"; trap - EXIT; return 1; } prompt="$(cat </dev/null)" applied="$(printf '%s' "$result" | python3 "${SCRIPT_DIR}/memory.py" apply --role "$ROLE" 2>/dev/null)" if [ -z "$applied" ]; then role_log "ERR" "整理結果無法套用(角色 ${ROLE}),保留待整理記憶到下個週期" role_lock_release "$ROLE" trap - EXIT return 1 fi role_log "INF" "記憶整理完成(角色 ${ROLE},觸發:${reason}):${applied}" forgotten="$(python3 "${SCRIPT_DIR}/memory.py" forget --role "$ROLE" 2>/dev/null | tr '\n' ';')" role_log "INF" "遺忘檢查:${forgotten}" role_lock_release "$ROLE" trap - EXIT return 0 } # ------------------------------------------------------------------------------ # 排程安裝:cron 環境沒有互動 shell 的環境變數,需把必要變數與 PATH 一併寫入 # ------------------------------------------------------------------------------ cron_quote() { # 把值包成單引號:PATH 等變數常含空白(例如 /mnt/c/Program Files), # 未加引號會被 cron 的 sh 拆成指令;% 是 cron 的換行符號,一律跳脫。 printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g; s/%/\\\\%/g")" } cron_line() { # 組出 crontab 條目:睡眠時段內每小時檢查一次 local env_prefix="PATH=$(cron_quote "$PATH")" local var for var in ROLE_ENABLED ROLE_NAME ROLE_HOME ROLE_MEMORY_HOME ROLE_CLI ROLE_MODEL ROLE_SLEEP_START ROLE_SLEEP_END ROLE_SCOPE; do if [ -n "${!var:-}" ]; then env_prefix="${env_prefix} ${var}=$(cron_quote "${!var}")" fi done printf '0 %s * * * %s %s --run >> %s 2>&1 %s\n' \ "$(cron_hours)" "$env_prefix" "$(cron_quote "${SCRIPT_DIR}/role_sleep.sh")" \ "$(cron_quote "$(sleep_log_path)")" "$CRON_MARKER" } cron_hours() { # 依睡眠時段換算 cron 小時欄位(每小時檢查一次,讓 AI 運行中的情況能在下個小時重試) local start end hour hours="" start="$(role_time_to_minutes "$(role_sleep_start)")" || { printf '22-23,0-5'; return 0; } end="$(role_time_to_minutes "$(role_sleep_end)")" || { printf '22-23,0-5'; return 0; } start=$((start / 60)) end=$((end / 60)) hour="$start" while [ "$hour" != "$end" ]; do hours="${hours}${hours:+,}${hour}" hour=$(((hour + 1) % 24)) done printf '%s' "${hours:-22,23,0,1,2,3,4,5}" } sleep_log_path() { # 排程輸出的 log 路徑(只記狀態訊息,不含記憶內容) printf '%s/sleep.log' "$(role_home)" } install_cron() { # 安裝或更新睡眠排程;以 marker 註解辨識自己的條目,不動使用者其他排程 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")" new="$(printf '%s\n%s' "$current" "$(cron_line)" | sed '/^$/d')" printf '%s\n' "$new" | crontab - || { role_log "ERR" "寫入 crontab 失敗"; return 1; } role_log "INF" "已安裝睡眠排程:每日 $(cron_hours) 時整點檢查(角色 ${ROLE},時段 $(role_sleep_start)–$(role_sleep_end))" 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 的背景補跑仍會運作" fi return 0 } 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" | crontab - role_log "INF" "已移除睡眠排程" return 0 } show_status() { # 以表格輸出目前角色與記憶狀態(供 skill 的 --status 使用) local cron_state="未安裝" cron_service="未執行" window="否" crontab -l 2>/dev/null | grep -qF "$CRON_MARKER" && cron_state="已安裝" { pgrep -x cron >/dev/null 2>&1 || pgrep -x crond >/dev/null 2>&1; } && cron_service="執行中" role_in_sleep_window && window="是" printf '| 項目 | 值 |\n| --- | --- |\n' printf '| 角色 | %s |\n' "$ROLE" printf '| 角色定義檔 | %s |\n' "$(role_file "$ROLE")" printf '| 睡眠時段 | %s–%s |\n' "$(role_sleep_start)" "$(role_sleep_end)" printf '| 目前是否睡眠中 | %s |\n' "$window" printf '| cron 排程 | %s |\n' "$cron_state" printf '| cron 服務 | %s |\n' "$cron_service" printf '| 摘要 CLI | %s |\n' "$(role_select_cli 2>/dev/null || printf '找不到可用 CLI')" printf '\n' python3 "${SCRIPT_DIR}/memory.py" stats --role "$ROLE" 2>/dev/null printf '\n' } # ------------------------------------------------------------------------------ # 進入點 # ------------------------------------------------------------------------------ MODE="${1:---status}" case "$MODE" in --run) role_enabled || exit 0 require_role if ! role_in_sleep_window; then role_log "DBG" "目前不在睡眠時段($(role_sleep_start)–$(role_sleep_end)),略過" exit 0 fi if role_ai_running; then role_log "INF" "偵測到 AI 正在運行,本小時不進入睡眠,下個整點再檢查" exit 0 fi sleep_cycle "cron" ;; --catchup) role_enabled || exit 0 require_role if [ "$(python3 "${SCRIPT_DIR}/memory.py" need-sleep --role "$ROLE" 2>/dev/null)" != "yes" ]; then role_log "DBG" "不需補跑整理" exit 0 fi sleep_cycle "補跑" ;; --force) require_role sleep_cycle "手動" ;; --install-cron) require_role install_cron ;; --remove-cron) remove_cron ;; --status) require_role show_status ;; -h|--help) usage ;; *) usage exit 1 ;; esac