@@ -7,7 +7,7 @@
# 另提供 --nap(CLI 閒置時的小睡整理)、--catchup(cron 未執行時的補跑)、
# --force(手動立即整理)、--export(匯出角色壓縮檔)、
# --install-cron/ --remove-cron(排程安裝與移除)、--status(狀態)。
# 更新時間:2026/07/28 16:02:10
# 更新時間:2026/07/29 13:13:21
# 相依:bash、node、任一 headless CLI、crontab(僅排程安裝需要)、
# 同目錄的 role_lib.sh 與 memory.js。
# 退出碼:0 成功或無事可做;1 參數錯誤或整理失敗(cron 觸發時不影響使用者)。
@@ -256,14 +256,14 @@ cron_env_prefix() {
cron_line( ) {
# 組出 crontab 條目:睡眠時段內每小時檢查一次
printf '0 %s * * * %s %s --run >> %s 2>&1 %s\n' \
" $( cron_hours) " " $( cron_env_prefix) " " $( cron_quote " ${ SCRIPT_DIR } /role_sleep.sh " ) " \
" $( cron_hours) " " $( cron_env_prefix) " " $( cron_quote " $( launcher_path ) " ) " \
" $( cron_quote " $( sleep_log_path) " ) " " $CRON_MARKER "
}
nap_cron_line( ) {
# 組出小睡 crontab 條目:全天依間隔檢查閒置狀態
printf '*/%s * * * * %s %s --nap >> %s 2>&1 %s\n' \
" $( nap_interval_minutes) " " $( cron_env_prefix) " " $( cron_quote " ${ SCRIPT_DIR } /role_sleep.sh " ) " \
" $( nap_interval_minutes) " " $( cron_env_prefix) " " $( cron_quote " $( launcher_path ) " ) " \
" $( cron_quote " $( sleep_log_path) " ) " " $NAP_CRON_MARKER "
}
@@ -297,7 +297,7 @@ brief_hour() {
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 " ) " \
" $( brief_hour) " " $( cron_env_prefix) " " $( cron_quote " $( launcher_path ) " ) " \
" $( cron_quote " $( sleep_log_path) " ) " " $BRIEF_CRON_MARKER "
}
@@ -390,10 +390,64 @@ sleep_log_path() {
printf '%s/sleep.log' " $( role_home) "
}
launcher_path( ) {
# 排程啟動器:路徑固定不含版本號,crontab 條目一律指向這裡
printf '%s/bin/role_sleep_launcher.sh' " $( role_home) "
}
shell_quote( ) {
# 包成單引號供 shell script 內文使用;與 cron_quote 的差別是不跳脫 %
printf "'%s'" " $( printf '%s' " $1 " | sed "s/'/'\\\\''/g" ) "
}
write_cron_launcher( ) {
# 產生排程啟動器:cron 條目指向它,真正要執行的 role_sleep.sh 在觸發當下才解析。
#
# 為什麼要多這一層:若把安裝當下的版本目錄直接寫進 crontab,plugin 升版、
# 舊版本目錄被清掉之後,排程就會指向不存在的路徑並**靜默失效**
# (同一類錯誤曾造成排程長期空轉,且因為 cron 不會回報而不易察覺)。
local path dir
path = " $( launcher_path) "
dir = " $( dirname " $path " ) "
mkdir -p " $dir " 2>/dev/null || { role_log "ERR" " 無法建立啟動器目錄: ${ dir } " ; return 1; }
{
printf '#!/usr/bin/env bash\n'
printf '# 由 role_sleep.sh --install-cron 自動產生,請勿手動編輯(重跑 --install-cron 會覆蓋)。\n'
printf '# 用途:讓 crontab 條目指向固定路徑,實際執行的版本於觸發當下解析,plugin 升版後不必重裝排程。\n'
printf '# 更新時間:%s\n' " $( TZ = 'Asia/Taipei' date '+%Y/%m/%d %H:%M:%S' ) "
cat <<'EOF_LAUNCHER'
set -uo pipefail
resolve_latest() {
# 同一個 cache 根目錄下可能留有多個版本目錄,取版本號最大者
ls -d "$1"/*/jsc-generic/*/scripts/role/role_sleep.sh 2>/dev/null | sort -V | tail -n 1
}
# 以 Claude Code 端為優先,沒有才找 Codex 端;兩端腳本相同,差別只在安裝位置
target="$(resolve_latest "${HOME}/.claude/plugins/cache")"
[ -n "$target" ] || target="$(resolve_latest "${HOME}/.codex/plugins/cache")"
EOF_LAUNCHER
printf '[ -n "$target" ] || target=%s # 後援:安裝當下的位置\n' " $( shell_quote " ${ SCRIPT_DIR } /role_sleep.sh " ) "
cat <<'EOF_LAUNCHER'
if [ ! -r "$target" ]; then
printf '[role-sleep][ERR]: 找不到可用的 role_sleep.sh,本次排程略過\n' >&2
exit 1
fi
exec bash "$target" "$@"
EOF_LAUNCHER
} > " $path " || { role_log "ERR" " 寫入啟動器失敗: ${ path } " ; return 1; }
chmod +x " $path " 2>/dev/null
return 0
}
install_cron( ) {
# 安裝或更新睡眠排程;以 marker 註解辨識自己的條目,不動使用者其他排程
command -v crontab >/dev/null 2>& 1 || { role_log "ERR" "找不到 crontab,無法安裝排程" ; return 1; }
mkdir -p " $( role_home) " 2>/dev/null
# 先產生啟動器:cron 條目只認這個固定路徑,實際版本留到觸發當下才解析
write_cron_launcher || return 1
local current new
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' ) "
@@ -416,6 +470,7 @@ install_cron() {
else
role_log "DBG" " 未安裝晨間狀態檢查排程(需建立 $( checks_dir) 並放入可執行的 *.sh) "
fi
role_log "INF" " 排程啟動器: $( launcher_path) (升版後不必重裝排程) "
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 的背景補跑仍會運作"
@@ -427,6 +482,13 @@ 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 " | grep -v -F " $BRIEF_CRON_MARKER " | crontab -
# 啟動器只服務本 skill 的排程,排程移除後一併清掉;bin/ 若還有別的檔案則保留
local launcher
launcher = " $( launcher_path) "
if [ -f " $launcher " ] ; then
rm -f " $launcher " && role_log "INF" " 已移除排程啟動器: ${ launcher } "
fi
rmdir " $( dirname " $launcher " ) " 2>/dev/null || true
role_log "INF" "已移除睡眠、小睡與晨間狀態檢查排程"
return 0
}
@@ -624,6 +686,25 @@ EOF_AGENT
return 0
}
cron_target_state( ) {
# 檢查 crontab 條目實際指向的執行檔還在不在。
# 舊條目若寫死版本目錄,plugin 升版清掉舊版本後就會指向不存在的路徑並靜默失效,
# cron 不會回報,只能在這裡主動點出來。
local line target
line = " $( crontab -l 2>/dev/null | grep -F " $CRON_MARKER " | head -n 1) "
[ -n " $line " ] || { printf '未安裝' ; return 0; }
target = " $( printf '%s' " $line " | sed -n "s/.*'\([^']*role_sleep[^']*\)'[[:space:]]*--.*/\1/p" ) "
if [ -z " $target " ] ; then
printf '無法解析條目內容'
elif [ ! -r " $target " ] ; then
printf '⚠ 指向不存在的路徑(%s),請重跑 --install-cron' " $target "
elif [ " $target " = " $( launcher_path) " ] ; then
printf '正常(%s) ' " $target "
else
printf '⚠ 舊式寫死版本路徑(%s),建議重跑 --install-cron' " $target "
fi
}
show_status( ) {
# 以表格輸出目前角色與記憶狀態(供 skill 的 --status 使用)
local cron_state = "未安裝" nap_state = "未安裝" brief_state = "未安裝" cron_service = "未執行" window = "否" checks_state instance_state
@@ -661,6 +742,7 @@ show_status() {
printf '| cron 排程 | %s |\n' " $cron_state "
printf '| 小睡排程 | %s |\n' " $nap_state "
printf '| 晨間檢查排程 | %s |\n' " $brief_state "
printf '| 排程指向 | %s |\n' " $( cron_target_state) "
printf '| 角色載入鎖 | %s |\n' " $instance_state "
printf '| 檢查腳本目錄 | %s |\n' " $checks_state "
printf '| 小睡啟用 | %s |\n' " $( nap_enabled && printf '是' || printf '否' ) "