fix(role): cron 條目改指固定路徑啟動器,升版不再靜默失效
--install-cron 原本把安裝當下的版本目錄寫進 crontab,plugin 升版、 舊版本目錄被清掉之後,排程會指向不存在的路徑而靜默停擺(cron 不回報, 此類錯誤曾造成排程長期空轉)。 - 新增 ~/.roles/bin/role_sleep_launcher.sh:crontab 只認這個固定路徑, 實際的 role_sleep.sh 於觸發當下以 sort -V 解析最新版本後 exec - 解析順序 Claude Code 端 → Codex 端,都找不到才退回安裝當下的路徑 - --remove-cron 一併清除啟動器 - --status 新增「排程指向」欄位,主動指出失效或舊式寫死路徑的條目 - SKILL.md 補上啟動器機制與轉換方式 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 '否')"
|
||||
|
||||
@@ -395,6 +395,7 @@ node "${ROLE_DIR}/memory.js" forget --role "<角色 ID>" --dry-run
|
||||
| 時段 | 目前是否落在睡眠時段(睡眠時本來就不載入角色) |
|
||||
| 依賴 | `node` 與 `ROLE_CLI` 選到的 CLI 是否找得到 |
|
||||
| 排程 | cron 條目是否存在、cron 服務是否執行中(WSL 常未啟動 → 靠啟動時補跑) |
|
||||
| 排程指向 | 條目指到的執行檔是否還存在(舊條目寫死版本目錄時會在升版後失效,見下節) |
|
||||
|
||||
### `--install-cron`/`--remove-cron`
|
||||
|
||||
@@ -406,6 +407,25 @@ node "${ROLE_DIR}/memory.js" forget --role "<角色 ID>" --dry-run
|
||||
|
||||
安裝時會把精簡後的 `PATH`(系統基本路徑、`node` 與摘要 CLI 所在目錄)與 `ROLE_*` 變數固定寫進條目(cron 沒有互動 shell 的環境變數),並在 cron 服務未執行時警告。不得把互動 shell 的完整 `PATH` 原樣寫入,避免 crontab 因單行過長拒收。
|
||||
|
||||
#### 排程啟動器(為什麼 crontab 不直接指向 `role_sleep.sh`)
|
||||
|
||||
crontab 條目指向的是 `~/.roles/bin/role_sleep_launcher.sh`,這支啟動器由 `--install-cron` 自動產生(`--remove-cron` 會一併刪除),內容只做一件事:解析目前最新的 `role_sleep.sh` 後 `exec` 過去。
|
||||
|
||||
| 位置 | 是否含版本號 |
|
||||
| --- | --- |
|
||||
| crontab 條目 → 啟動器 | ❌ 固定路徑 |
|
||||
| 啟動器 → 實際腳本 | ✅ 觸發當下才解析 |
|
||||
|
||||
**理由**:plugin 每次升版都會產生新的版本目錄,舊目錄清掉後,寫死版本路徑的 crontab 條目就會指向不存在的檔案。cron 不會回報這種失敗,排程只是**靜默停擺**(此類錯誤曾造成排程長期空轉才被發現)。多這一層之後,升版不必重裝排程。
|
||||
|
||||
解析順序為 Claude Code 端 → Codex 端,各自取版本號最大者(`sort -V`),都找不到才退回安裝當下的路徑:
|
||||
|
||||
```bash
|
||||
ls -d "$HOME"/.claude/plugins/cache/*/jsc-generic/*/scripts/role/role_sleep.sh | sort -V | tail -n 1
|
||||
```
|
||||
|
||||
舊版安裝的排程仍是寫死路徑,`--status` 的「排程指向」欄位會標示出來,重跑一次 `--install-cron` 即可轉換。
|
||||
|
||||
---
|
||||
|
||||
## 角色檔標準格式
|
||||
|
||||
Reference in New Issue
Block a user