釋出 jsc-assist 0.1.1:排程輪次自帶環境與權限,監控頁改成固定三塊 #8
+171
-29
@@ -60,6 +60,36 @@
|
||||
# 條目一律接 `</dev/null`,跑的東西讀不到標準輸入,卡不住。heartbeat.sh 本來就不讀 stdin,
|
||||
# 這一條是給巡檢那一筆用的:助理跳出權限詢問就會整輪卡死,等於排程沒跑。
|
||||
#
|
||||
# --- 排程那一輪的環境跟你現在這個殼不一樣 ---
|
||||
#
|
||||
# cron 給的 PATH 很短,多半只有 /usr/bin 與 /bin,裝在 ~/.local/bin 的 CLI 一律 not found。
|
||||
# cron 也不讀 .bashrc,所以殼裡設好的 GITEA_HOST、GITEA_TOKEN、JSC_WIKI_REPO_* 那一輪全是
|
||||
# 空的,wiki 連不上,監控頁寫不成,巡檢就不寫心跳,整條排程空轉。
|
||||
# 所以條目自己把環境帶齊,兩件事:
|
||||
# 一、CLI 用 `command -v` 解成絕對路徑再寫進條目,解不到就回 6 停下,不猜路徑——猜錯只是
|
||||
# 把「每輪 not found」換成「每輪跑錯東西」,一樣沒有人在現場看得到。
|
||||
# 二、要用到的環境變數在 install 當下就地快照,以 `名稱='值'` 寫成條目的指令前綴。
|
||||
# 條目裡不 source 任何設定檔:很多機器的 .bashrc 開頭就是「非互動就 return」,那一招在 cron
|
||||
# 底下會安靜失效,失效的樣子跟沒設一模一樣,查起來要花一整輪。快照的代價是條目寫死了安裝
|
||||
# 當下的值,環境變數改過就要重跑一次 install,條目才會跟著換。
|
||||
# 條目裡帶著金鑰快照,crontab 檔案要保持只有本人讀得到;本腳本印出條目時一律把金鑰遮掉。
|
||||
#
|
||||
# --- 無人值守那一輪要自己帶寫入確認 ---
|
||||
#
|
||||
# jsc-gitea 的寫入確認只認 tty,而條目帶 `</dev/null`,排程那一輪永遠沒有 tty,寫 wiki 一定
|
||||
# 被擋。頁寫不成就不寫心跳,於是排程裝著卻永遠空轉。所以條目自帶 JSC_GITEA_CONFIRM=yes:
|
||||
# 無人值守的那一輪本來就沒有人可以按同意,擋下來也沒有人會看到。
|
||||
#
|
||||
# --- 裝完要開哪些權限 ---
|
||||
#
|
||||
# 排程那一輪跑在沒有人的工作階段,跳出權限詢問就是卡住整輪,而且卡到鎖逾時才有下一輪。
|
||||
# install 成功之後會把那一輪需要的 allow 規則印出來,一行一條。
|
||||
# 路徑一律走 $JSC_HOME/current/{外掛名}:那是一組不帶版本的連結,指向該外掛在快取裡的最新
|
||||
# 版,由 deploy 維護。規則就是那組確切路徑,比對得準,外掛升版也不用回頭改設定。
|
||||
# 檔案寫入只認 Edit(...),Write(...) 規則沒有作用,所以不印 Write。
|
||||
# 連結不在就先警告:那一輪會因為找不到工具而失敗。連結由 deploy 建,本腳本不代建——排程
|
||||
# 腳本自己去補外掛的部署結構,等於兩個地方管同一件事,壞掉的時候查不出是誰建的。
|
||||
#
|
||||
# --- log 放哪裡 ---
|
||||
#
|
||||
# $JSC_HOME/assistant/schedule.log(JSC_HOME 未設定時為 ~/.jsc)。刻意放在專案外面:寫進
|
||||
@@ -71,12 +101,16 @@
|
||||
# 假的 crontab 驗濾除邏輯時才設
|
||||
# JSC_ASSIST_PATROL_CMD 巡檢要跑的指令,優先於 --patrol-cmd 以外的所有推斷
|
||||
# JSC_CLI 目前是哪一支 CLI,決定巡檢預設指令
|
||||
# JSC_ASSISTANT_HEARTBEAT_TTL 心跳過期門檻,單位秒。巡檢週期由它算出來
|
||||
# JSC_ASSISTANT_HEARTBEAT_TTL 心跳過期門檻,單位秒。巡檢週期由它算出來,也會快照進條目
|
||||
# GITEA_HOST GITEA_TOKEN wiki 連線用。install 當下快照進條目
|
||||
# JSC_WIKI_REPO wiki 存取庫預設值。install 當下快照進條目
|
||||
# JSC_WIKI_REPO_{TYPE} 各頁型的 wiki 存取庫。已設定的全部快照進條目
|
||||
set -u
|
||||
|
||||
MARK_PREFIX='# jsc-assist:assistant'
|
||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||
STATE_DIR="$JSC_HOME/assistant"
|
||||
CURRENT="$JSC_HOME/current"
|
||||
LOG="$STATE_DIR/schedule.log"
|
||||
CRONTAB_CMD="${JSC_ASSIST_CRONTAB_CMD:-crontab}"
|
||||
TASK_PREFIX='jsc-assist-assistant'
|
||||
@@ -105,12 +139,17 @@ die() { # $1=結束碼 $2=訊息
|
||||
|
||||
note() { printf '[jsc][助理排程]:%s\n' "$1" >&2; }
|
||||
|
||||
# 找出 jsc-hooks 的 hooks/heartbeat.sh 絕對路徑。搜尋順序比照 jsc-hooks lib.sh 的
|
||||
# jsc_gitea_sh():先環境變數,再開發用的並排存取庫版面,最後已安裝的 plugin 快取版面。
|
||||
# 找出 jsc-hooks 的 hooks/heartbeat.sh 絕對路徑。搜尋順序:先環境變數覆寫,再
|
||||
# $JSC_HOME/current 那一組連結,然後開發用的並排存取庫版面,最後已安裝的 plugin 快取版面。
|
||||
# current 排在快取前面是刻意的:技能與權限規則都以 current 為準,腳本內部再自己去挑另一個
|
||||
# 版本,同一輪就會跑到混版的工具,而那種不一致查起來沒有任何線索。
|
||||
heartbeat_sh() {
|
||||
if [ -n "${JSC_HOOKS_HOOKS:-}" ] && [ -f "$JSC_HOOKS_HOOKS/heartbeat.sh" ]; then
|
||||
printf '%s\n' "$JSC_HOOKS_HOOKS/heartbeat.sh"; return 0
|
||||
fi
|
||||
if [ -f "$CURRENT/jsc-hooks/hooks/heartbeat.sh" ]; then
|
||||
printf '%s\n' "$CURRENT/jsc-hooks/hooks/heartbeat.sh"; return 0
|
||||
fi
|
||||
_root="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
|
||||
for _c in "$_root/../hooks/hooks/heartbeat.sh" "$_root/../jsc-hooks/hooks/heartbeat.sh"; do
|
||||
[ -f "$_c" ] && { (CDPATH= cd -- "$(dirname -- "$_c")" && printf '%s/heartbeat.sh\n' "$(pwd)"); return 0; }
|
||||
@@ -206,28 +245,74 @@ spec_of() {
|
||||
}
|
||||
|
||||
# 巡檢要跑的指令。優先序:--patrol-cmd 或 JSC_ASSIST_PATROL_CMD > 依 CLI 代號推斷。
|
||||
# 判不出 CLI 就回非 0,由主流程回 6,不猜——猜錯會每 15 分鐘跑一支不存在的執行檔。
|
||||
# 判不出 CLI,或那一支的執行檔不在 PATH 上,都回非 0 由主流程回 6,不猜。
|
||||
# 執行檔一律用 `command -v` 解成絕對路徑:cron 的 PATH 只有 /usr/bin 與 /bin,裸的指令名
|
||||
# 每一輪都是 not found,而那一輪不會有人看到錯誤訊息。
|
||||
patrol_command() {
|
||||
[ -n "$PATROL_CMD" ] && { printf '%s' "$PATROL_CMD"; return 0; }
|
||||
_cli="$CLI"
|
||||
[ -n "$_cli" ] || _cli="${JSC_CLI:-}"
|
||||
[ -n "$_cli" ] || { [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && _cli=claude; }
|
||||
case "$_cli" in
|
||||
claude) printf 'claude -p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
||||
codex) printf "codex exec '\$assistant 跑一輪巡檢'" ;;
|
||||
copilot) printf 'copilot -p "跑一輪助理巡檢"' ;;
|
||||
antigravity) printf 'agy -p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
||||
kiro) printf 'kiro-cli -p "跑一輪助理巡檢"' ;;
|
||||
*) return 1 ;;
|
||||
claude) _bin='claude'; _args='-p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
||||
codex) _bin='codex'; _args="exec '\$assistant 跑一輪巡檢'" ;;
|
||||
copilot) _bin='copilot'; _args='-p "跑一輪助理巡檢"' ;;
|
||||
antigravity) _bin='agy'; _args='-p "/jsc-assist:assistant 跑一輪巡檢"' ;;
|
||||
kiro) _bin='kiro-cli'; _args='-p "跑一輪助理巡檢"' ;;
|
||||
*) printf '判不出要用哪一支 CLI 跑巡檢,請帶 --cli {claude|codex|copilot|antigravity|kiro} 或 --patrol-cmd「指令」。\n' >&2
|
||||
return 1 ;;
|
||||
esac
|
||||
_abs=$(command -v "$_bin" 2>/dev/null) || _abs=''
|
||||
# 內建指令與別名也會被 command -v 認出來,但那些寫進 cron 沒有意義,只收絕對路徑。
|
||||
case "$_abs" in
|
||||
/*) ;;
|
||||
*) printf '這台機器的 PATH 上找不到 %s 的執行檔,排程條目沒有絕對路徑可以寫。請先確認 %s 裝好了,或改用 --patrol-cmd 指定完整指令。\n' "$_cli" "$_bin" >&2
|
||||
return 1 ;;
|
||||
esac
|
||||
printf "'%s' %s" "$_abs" "$_args"
|
||||
}
|
||||
|
||||
# 要快照進條目的環境變數名稱。前四支是巡檢那一輪一定要用到的;JSC_WIKI_REPO 系列逐台機器
|
||||
# 不同,直接從現在的環境撈出所有已設定的,不寫死清單。
|
||||
snapshot_names() {
|
||||
printf '%s\n' GITEA_HOST GITEA_TOKEN JSC_HOME JSC_ASSISTANT_HEARTBEAT_TTL
|
||||
env 2>/dev/null \
|
||||
| sed -n 's/^\(JSC_WIKI_REPO\)=.*/\1/p; s/^\(JSC_WIKI_REPO_[A-Za-z0-9_]*\)=.*/\1/p' \
|
||||
| sort -u
|
||||
}
|
||||
|
||||
# 把值包成單引號字串。值裡的單引號照 POSIX 的 '\'' 寫法拆開再接回來,不然一個引號就把
|
||||
# 整條 cron 指令切斷。
|
||||
shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
|
||||
|
||||
# 條目的指令前綴:固定三個旗標,加上這一刻的環境快照。沒設定的變數直接跳過,不寫空值——
|
||||
# 寫 NAME='' 進去,讀的人分不出是「刻意設成空」還是「安裝時忘了設」。
|
||||
# JSC_GITEA_CONFIRM=yes 是必要的:寫入確認只認 tty,排程那一輪沒有 tty,不帶這個旗標監控頁
|
||||
# 一定寫不成,而頁寫不成就不寫心跳,排程等於永遠空轉。
|
||||
env_prefix() { # 順便把快照到的變數名稱寫進 $1,供輸出列出名稱(只有名稱,不含值)
|
||||
_p='JSC_CLI=cron JSC_SESSION_ID=schedule JSC_GITEA_CONFIRM=yes'
|
||||
_names=''
|
||||
for _n in $(snapshot_names); do
|
||||
eval "_v=\${$_n:-}"
|
||||
[ -n "$_v" ] || continue
|
||||
_p="$_p $_n=$(shq "$_v")"
|
||||
if [ -z "$_names" ]; then _names="$_n"; else _names="$_names,$_n"; fi
|
||||
done
|
||||
[ -n "${1:-}" ] && printf '%s' "$_names" >"$1"
|
||||
printf '%s' "$_p"
|
||||
}
|
||||
|
||||
# 條目裡有金鑰快照,原樣印出來就是把金鑰留在對話紀錄與 log 裡,所以印之前先遮掉值。
|
||||
# 遮到下一個空白為止,不遮到下一個單引號:值裡的單引號會跳脫成 '\'',遇到引號就收手會把
|
||||
# 金鑰的後半段漏出來。Gitea 的金鑰不含空白,所以以空白為界是安全的。
|
||||
mask_secret() { sed "s/GITEA_TOKEN=[^ ]*/GITEA_TOKEN='***'/g"; }
|
||||
|
||||
# 組出一筆 crontab 條目。`%` 在 crontab 是換行符號,一律跳脫。
|
||||
cron_entry() { # $1=工作代號
|
||||
_spec=$(spec_of "$1")
|
||||
case "$1" in
|
||||
heartbeat) _cmd="JSC_CLI=cron JSC_SESSION_ID=schedule '$HEARTBEAT' write" ;;
|
||||
patrol) _cmd="JSC_CLI=cron JSC_SESSION_ID=schedule $PATROL_RESOLVED" ;;
|
||||
patrol) _cmd="$ENV_PREFIX $PATROL_RESOLVED" ;;
|
||||
esac
|
||||
printf '%s %s </dev/null >>%s 2>&1 %s' \
|
||||
"$_spec" "$_cmd" "'$LOG'" "$(marker_of "$1")" | sed 's/%/\\%/g'
|
||||
@@ -301,18 +386,69 @@ else
|
||||
PERIOD=$(period_for_ttl "$TTL")
|
||||
fi
|
||||
|
||||
# 巡檢指令在這裡就解出來。放進 cron_entry 再解的話,那支是在命令替換的子行程裡跑,
|
||||
# 判不出 CLI 時 die 只結束子行程,主流程會帶著空指令繼續往下裝。
|
||||
PATROL_RESOLVED=''
|
||||
case "$CMD:$JOBS" in
|
||||
install:*patrol*)
|
||||
PATROL_RESOLVED=$(patrol_command) \
|
||||
|| die 6 '判不出要用哪一支 CLI 跑巡檢,請帶 --cli {claude|codex|copilot|antigravity|kiro} 或 --patrol-cmd「指令」。' ;;
|
||||
esac
|
||||
|
||||
TMPD=$(mktemp -d) || die 4 '建不出暫存目錄。'
|
||||
trap 'rm -rf "$TMPD"' EXIT
|
||||
|
||||
# 巡檢指令與環境快照在這裡就解出來。放進 cron_entry 再解的話,那支是在命令替換的子行程裡
|
||||
# 跑,判不出 CLI 時 die 只結束子行程,主流程會帶著空指令繼續往下裝。
|
||||
PATROL_RESOLVED=''
|
||||
ENV_PREFIX='JSC_CLI=cron JSC_SESSION_ID=schedule JSC_GITEA_CONFIRM=yes'
|
||||
SNAPSHOT_NAMES=''
|
||||
case "$CMD:$JOBS" in
|
||||
install:*patrol*)
|
||||
if ! PATROL_RESOLVED=$(patrol_command 2>"$TMPD/cmd.err"); then
|
||||
_why=$(tr '\n' ' ' <"$TMPD/cmd.err" 2>/dev/null)
|
||||
[ -n "$_why" ] || _why='判不出要用哪一支 CLI 跑巡檢,請帶 --cli {claude|codex|copilot|antigravity|kiro} 或 --patrol-cmd「指令」。'
|
||||
die 6 "$_why"
|
||||
fi
|
||||
ENV_PREFIX=$(env_prefix "$TMPD/snapnames")
|
||||
SNAPSHOT_NAMES=$(cat "$TMPD/snapnames" 2>/dev/null) ;;
|
||||
esac
|
||||
|
||||
# --- 裝完要開的權限 ---
|
||||
|
||||
# 排程那一輪跑在沒有人的工作階段:跳出一次權限詢問就是整輪卡住,卡到鎖逾時才有下一輪。
|
||||
# 這一輪會用到的三支腳本,各印裸路徑、`sh 路徑`、`bash 路徑` 三種呼叫形式——同一支腳本換
|
||||
# 一種叫法就是另一條規則,少印一種就會在那一種叫法上卡住。
|
||||
# 路徑一律是 $JSC_HOME/current/{外掛名} 那一組,不是這支腳本現在被放在哪裡:規則放行的是
|
||||
# 那一組路徑,巡檢那一輪也只能用那一組路徑去叫工具,兩邊對得起來才不會被靜靜擋掉。
|
||||
print_allow_rules() {
|
||||
# gitea.sh 一定要有自己這一條。`Skill(jsc-gitea:wiki)` 只放行「叫用那支技能」,技能裡的
|
||||
# 每一個 Bash 呼叫仍然各自受檢,少了這一條,那一輪會在寫監控頁時靜靜被擋——頁寫不成就
|
||||
# 不寫心跳,外面只看得到心跳過期,看不出是權限擋的。
|
||||
for _s in "$CURRENT/jsc-assist/tools/schedule.sh" \
|
||||
"$CURRENT/jsc-assist/tools/patrol.sh" \
|
||||
"$CURRENT/jsc-hooks/hooks/heartbeat.sh" \
|
||||
"$CURRENT/jsc-gitea/tools/gitea.sh"; do
|
||||
printf 'allow_rule=Bash(%s:*)\n' "$_s"
|
||||
printf 'allow_rule=Bash(sh %s:*)\n' "$_s"
|
||||
printf 'allow_rule=Bash(bash %s:*)\n' "$_s"
|
||||
done
|
||||
# 檔案規則寫的是解出來的絕對路徑,不是 $JSC_HOME/assistant:設定檔不展開變數,寫變數名
|
||||
# 等於這條規則永遠比對不到。
|
||||
printf 'allow_rule=Read(%s/**)\n' "$STATE_DIR"
|
||||
printf 'allow_rule=Edit(%s/**)\n' "$STATE_DIR"
|
||||
printf 'allow_rule=Skill(jsc-gitea:wiki)\n'
|
||||
}
|
||||
|
||||
# 巡檢那一輪只用 $JSC_HOME/current 那一組連結叫工具,連結不在就是那一輪一定失敗。這裡只查
|
||||
# 與警告,不代建:連結是 deploy 的職責,兩個地方都在建同一組連結,壞掉時查不出是誰建的。
|
||||
check_current_links() {
|
||||
_miss=''; _paths=''
|
||||
for _p in jsc-assist jsc-gitea; do
|
||||
[ -e "$CURRENT/$_p" ] && continue
|
||||
if [ -z "$_miss" ]; then _miss="$_p"; _paths="$CURRENT/$_p"
|
||||
else _miss="$_miss,$_p"; _paths="$_paths、$CURRENT/$_p"; fi
|
||||
done
|
||||
if [ -n "$_miss" ]; then
|
||||
printf 'current_links=missing:%s\n' "$_miss"
|
||||
note "找不到這幾個連結:$_paths。巡檢那一輪會照這一組路徑叫工具,連結不在就叫不到,那一輪一定失敗,也不會寫心跳。請先跑 deploy 把連結建起來——本腳本不代建。"
|
||||
return 0
|
||||
fi
|
||||
printf 'current_links=ok\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- schtasks(Windows)---
|
||||
|
||||
schtasks_install() {
|
||||
@@ -397,12 +533,12 @@ crontab_install() {
|
||||
|
||||
if [ "$DRYRUN" -eq 1 ]; then
|
||||
for _job in $JOBS; do
|
||||
printf 'dryrun=crontab job=%s entry=%s\n' "$_job" "$(cron_entry "$_job")"
|
||||
printf 'dryrun=crontab job=%s entry=%s\n' "$_job" "$(cron_entry "$_job" | mask_secret)"
|
||||
done
|
||||
printf 'dryrun=crontab action=write ttl=%s period=%s legacy_removed=%s others_kept=%s total_lines=%s\n' \
|
||||
"$TTL" "$PERIOD" "$_legacy" "$_others" "$(count_lines "$_new")"
|
||||
printf 'dryrun=crontab action=write ttl=%s period=%s legacy_removed=%s others_kept=%s total_lines=%s env_snapshot=%s\n' \
|
||||
"$TTL" "$PERIOD" "$_legacy" "$_others" "$(count_lines "$_new")" "${SNAPSHOT_NAMES:-無}"
|
||||
printf -- '--- 寫回後的 crontab ---\n'
|
||||
cat "$_new"
|
||||
mask_secret <"$_new"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -422,10 +558,10 @@ crontab_install() {
|
||||
|| die 5 "別人的排程條目從 $_others 筆變成 $_kept 筆,寫回不完整。"
|
||||
|
||||
for _job in $JOBS; do
|
||||
printf 'installed=%s entry=%s\n' "$_job" "$(cron_lines_for "$_chk" "$_job")"
|
||||
printf 'installed=%s entry=%s\n' "$_job" "$(cron_lines_for "$_chk" "$_job" | mask_secret)"
|
||||
done
|
||||
printf 'ttl=%s period=%s legacy_removed=%s others_kept=%s log=%s\n' \
|
||||
"$TTL" "$PERIOD" "$_legacy" "$_kept" "$LOG"
|
||||
printf 'ttl=%s period=%s legacy_removed=%s others_kept=%s env_snapshot=%s log=%s\n' \
|
||||
"$TTL" "$PERIOD" "$_legacy" "$_kept" "${SNAPSHOT_NAMES:-無}" "$LOG"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -454,7 +590,7 @@ crontab_remove() {
|
||||
if [ "$DRYRUN" -eq 1 ]; then
|
||||
printf 'dryrun=crontab action=remove jobs=%s removed=%s\n' "$JOBS" "$_hit"
|
||||
printf -- '--- 寫回後的 crontab ---\n'
|
||||
cat "$_new"
|
||||
mask_secret <"$_new"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -480,7 +616,7 @@ crontab_status() {
|
||||
printf 'mechanism=crontab service=%s ttl=%s period=%s log=%s\n' \
|
||||
"$(service_state)" "$TTL" "$PERIOD" "$LOG"
|
||||
for _job in heartbeat patrol; do
|
||||
_line=$(cron_lines_for "$_cur" "$_job")
|
||||
_line=$(cron_lines_for "$_cur" "$_job" | mask_secret)
|
||||
if [ -n "$_line" ]; then
|
||||
printf 'job=%s installed=yes entry=%s\n' "$_job" "$_line"
|
||||
else
|
||||
@@ -498,6 +634,12 @@ case "$CMD" in
|
||||
crontab) crontab_install ;;
|
||||
schtasks) schtasks_install ;;
|
||||
esac
|
||||
# 權限規則與快照提醒排在服務檢查前面:結束碼 1 那一條也是條目已經寫進去了,那台機器
|
||||
# 一樣要開權限,只是還要先把 cron 服務叫起來。
|
||||
print_allow_rules
|
||||
check_current_links
|
||||
note '上面這幾條 allow 規則要先開,排程那一輪才不會停在權限詢問——那一輪沒有人可以按同意。規則放行的是 $JSC_HOME/current 那一組路徑,巡檢也只能用那一組路徑叫工具。檔案寫入只認 Edit,Write 規則沒有作用。'
|
||||
note "條目帶著安裝當下的環境變數快照(${SNAPSHOT_NAMES:-無}),其中含 Gitea 金鑰:crontab 檔案請保持只有本人讀得到。這幾個變數改過就要重跑一次 install,條目才會跟著換。"
|
||||
[ "$DRYRUN" -eq 1 ] && exit 0
|
||||
_svc=$(service_state)
|
||||
if [ "$_svc" = stopped ]; then
|
||||
|
||||
Reference in New Issue
Block a user