feat(role): 新增角色人格與長期記憶 skill、hook 與腳本
SessionStart 載入角色與記憶、Stop 記錄對話成記憶,睡眠時段(預設 22:00–06:00) 由 cron 排程整理:分類六類、去重合併、設標籤與一句話總結、壓縮歸檔, 日常與其他依使用頻率遺忘;cron 未執行時由啟動路徑背景補跑。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+258
@@ -0,0 +1,258 @@
|
||||
#!/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 <<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
|
||||
Reference in New Issue
Block a user