Files
shared/scripts/role/role_sleep.sh
T
JefferyandClaude Opus 5 7ebcec0a8e feat(role): 新增角色人格與長期記憶 skill、hook 與腳本
SessionStart 載入角色與記憶、Stop 記錄對話成記憶,睡眠時段(預設 22:00–06:00)
由 cron 排程整理:分類六類、去重合併、設標籤與一句話總結、壓縮歸檔,
日常與其他依使用頻率遺忘;cron 未執行時由啟動路徑背景補跑。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:11:16 +08:00

259 lines
11 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
# ==============================================================================
# 用途:角色的睡眠與記憶整理。由 cron 於睡眠時段每小時觸發(--run),
# 先檢查是否有 AI 正在運行,沒有才進入睡眠並整理記憶:
# 分類(重要/興趣/新知/技能/日常/其他)→ 去重合併 → 設標籤與一句話
# 總結 → 壓縮內容歸檔 → 日常與其他依使用頻率遺忘。
# 另提供 --catchupcron 未執行時的補跑)、--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 <<EOF_PROMPT
你是角色「${ROLE}」的睡眠記憶整理器。輸入包含兩段:INBOX(本次待整理的記憶)與 EXISTING(既有記憶索引)。
請把 INBOX 整理成歸檔用的記憶,規則:
1. 只輸出一個 JSON 物件,不要前言、不要結語、不要 code fence,格式為:
{"memories":[{"action":"new","category":"skill","summary":"一句話總結","tags":["標籤1","標籤2"],"content":"- 要點\n- 要點","from":["inbox 的 id"]}]}
2. action 三選一:
- new:新的一則記憶。多則 INBOX 講同一件事時合成一筆,from 列出全部來源 id。
- merge:內容已被 EXISTING 中某則涵蓋或重複,填 target 為該既有 id,content 寫合併後的完整內容。
- drop:純雜訊、無保存價值,只需填 from。
3. category 六選一:important(重要)/interest(興趣)/news(新知)/skill(技能)/daily(日常)/other(其他)。
important 放長期偏好、規範、決策與身分背景;interest 放反覆關注的主題;news 放新事實與外部資訊;
skill 放可重複套用的做法;daily 放一次性例行工作;其餘歸 other。
4. **每一則 INBOX 的 id 都必須出現在某一筆的 from 中**,沒被提及的會留到下個睡眠週期重做。
5. content 壓縮成 5 行以內要點(每行以「- 」開頭),總長不超過 400 字,去除重複敘述與流水帳。
6. summary 一句話 40 字內;tags 2 至 4 個。全部使用繁體中文(台灣用語)。
7. 嚴禁輸出任何憑證與個資:token、密碼、API key、連線字串、Email、電話、姓名、身分證號。
素材:
${material}
EOF_PROMPT
)"
result="$(role_run_cli "$cli" "$prompt" "$SLEEP_TIMEOUT")"
if [ -z "$result" ]; then
role_log "ERR" "整理結果為空(CLI ${cli}),保留待整理記憶到下個週期"
role_lock_release "$ROLE"
trap - EXIT
return 1
fi
result="$(printf '%s' "$result" | python3 "${SCRIPT_DIR}/transcript.py" redact 2>/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