feat(seed): 依委派清單種入與重建助理的內建定期檢查項
待辦簿做好了但是空的,也沒有東西會去填它。內建的定期檢查項該排哪些、什麼時候排、多久一次,這些資料就在委派清單裡——每一支非不交的技能都有時間點與週期兩欄。這一輪把清單接成待辦簿的資料來源。 反查靠新增的一個欄位,它非有不可。清單移除一支之後要刪掉對應那筆,但待辦簿的識別碼是建立時間加標題的雜湊,跟技能名無關。拿標題當鍵等於把措辭變成介面,改一個字舊那筆就再也認不出來,於是每次重建都刪不掉舊的又加一筆新的,同一個檢查每輪做兩次。拿動作當鍵,提醒類那十一筆全是同一個字、一支都分不出來,而觸發類那幾筆會撞上使用者自己交辦、動作剛好是同一支技能的那一筆——撞上就是把使用者交辦的事當成內建項刪掉。所以另立一欄記那一筆對應哪一支技能。 因為要加欄位,寫入端只能改存放那一支——它是唯一寫得出待辦檔的入口,這一輪沒有自己寫檔。連帶補上移除那個操作:規格要求不留孤兒,而原本六個操作一個都刪不掉。它對使用者交辦的那幾筆一律擋下、除非人親自帶強制旗標,而種入這一支一次都不帶。擋在單一寫入者這裡最省,那條規定寫在呼叫端的話,呼叫端每多一個就要各自再實作一次。 交出方式與動作是兩套詞彙,對映是這一輪的重點。含觸發就取技能名,其餘一律取提醒。觸發的定義就是呼叫既有技能、內容照那支技能自己的流程走,所以填技能名等於照判定結果做。巡檢與提醒那兩種沒有獨立入口——沒有任何腳本或技能名代表得了某一支技能的唯讀切片——這時候填技能名,助理下一輪就會把整支技能一路跑完,那正是切片交要防的事。所以填提醒:照時程提醒、指出入口,不動手。實測十六筆裡五筆是技能名、十一筆是提醒。 條件式交的三支一律不種入,但逐支吵出來。條件本身是散文,清單裡沒有機器讀得懂的條件欄位,所以條件成立了沒有現在只有人答得出來。種進去的代價有現成例子:其中一支的條件明寫要等它自家路徑不再帶版本號,而那個條件現在不成立,種進去助理每輪都會叫它、每輪停在第一支自家腳本,沒有錯誤、沒有輸出、心跳照寫,看起來完全正常。一個會無聲卡死的項目比一個缺掉的項目難查得多。不種入的代價是看不出為什麼少了它,用逐支印一行保留原因補掉。人確認過某一支條件成立就一支一支帶旗標放行,不給全部放行的旗標,那等於用一個決定蓋掉三個不同的條件。 種入與重建是同一段程式,啟動時每次都跑。不記跑過沒有,也沒有第一次旗標,要不要動手完全由現況決定。分成兩段的話,兩段各自回答該有哪幾筆這同一個問題,等其中一段改了判準就會一邊加一邊刪同一筆,每輪反覆。啟動時跑實際套用、查現況時跑唯讀預覽,巡檢那一輪兩個都不跑——無人值守那一輪移除一筆會把那一筆的執行紀錄與失敗次數一起弄丟,而清單同步到一半就會刪錯,破壞性清理留給人。 清單讀不到的三種情況一筆都不移除。讀不到時,清單上沒有與這台機器沒裝那個外掛分不出來,照字面跑會把所有內建項一次刪光,而且結束碼看起來完全成功。 清單上還在、還可交,只是時間點或週期換了值的那種情況,規格三條規則一條都沒講到。處置是預設只報差異不改:存放那一支刻意沒有編輯操作,改值只能移除再重登,那會換識別碼、把執行紀錄與失敗次數歸零,一個已經連續失敗五次的項目會看起來像全新的。人要換就帶旗標。 跨外掛相依宣告到清單所在的那個外掛,版本下限取現行的發行版——那是清單與它的欄位說明都已經在上面的版本。寫更低的下限會讓一台裝著舊版、清單還不存在或欄位不同的機器通過相依檢查,然後在讀清單那一步才失敗。清單路徑照今天剛改的規則走,由叫用時餵進來的根目錄組成,那一支自己不解。 三份 manifest 的版號一併從 0.1.8 升到 0.1.9,並在相依欄加上清單所在的那個外掛。這一次沒有把版號分成獨立一筆:相依宣告與版號是同一個決定的兩半,宣告了新相依卻不升版,安裝端不會知道要重新檢查相依。
This commit is contained in:
Executable
+503
@@ -0,0 +1,503 @@
|
||||
#!/usr/bin/env sh
|
||||
# seed-tasks.sh — 依委派清單種入與重建助理的內建定期檢查項(供 jsc-assist:assistant 呼叫)。
|
||||
#
|
||||
# 用法:
|
||||
# seed-tasks.sh plan [--root {字面絕對路徑}] [--spec {清單檔}]
|
||||
# [--allow-cond jsc-{domain}:{技能名}]... [--refresh]
|
||||
# seed-tasks.sh apply [--root {字面絕對路徑}] [--spec {清單檔}]
|
||||
# [--allow-cond jsc-{domain}:{技能名}]... [--refresh]
|
||||
#
|
||||
# plan 唯讀預覽:算出該加哪幾筆、該移除哪幾筆、哪幾筆對不上,一筆都不寫。
|
||||
# apply 真的做:該加的用 tasks.sh add 加,該移除的用 tasks.sh remove 移除。
|
||||
#
|
||||
# 結束碼:
|
||||
# 0 對齊完成。plan 印完,或 apply 做完。零筆改動照樣是 0:清單與待辦簿本來就一致,
|
||||
# 不是失敗,也不必分流
|
||||
# 1 委派清單讀不到:找不到那個檔案,或檔案在卻讀不到內容。**一筆都不動**
|
||||
# 2 清單讀到了,卻解不出任何一列可交項目。**一筆都不動**,理由見下面「空清單一律不動手」
|
||||
# 3 找不到本 domain 的 tasks.sh。**一筆都不動**:待辦簿只有那一支寫得出來
|
||||
# 4 至少一筆 add 或 remove 失敗,其餘各筆照做完。哪一筆失敗、回了哪一碼,逐筆印出來
|
||||
# 5 檔案系統失敗:暫存目錄建不起來,或暫存檔寫不進去
|
||||
# 6 用法錯誤:不認得的子命令、不認得的選項、選項缺值、--allow-cond 的值形狀不對
|
||||
#
|
||||
# --- 這一支負責什麼、不負責什麼 ---
|
||||
#
|
||||
# 只負責「待辦簿裡的內建項要跟委派清單一致」這一件事。它不判到期(那是 due.sh)、不執行任何
|
||||
# 一筆待辦、不寫待辦檔(那是 tasks.sh,那一支是唯一的寫入入口,這裡一律去呼叫它,不自己動
|
||||
# tasks/ 底下的檔案)。
|
||||
# 種入與重建是同一段程式,不是兩段。理由:兩段程式各自回答「待辦簿裡該有哪幾筆」這個同一個
|
||||
# 問題,第一次答案相同看起來沒事,等其中一段改了判準,兩段就會一邊加一邊刪同一筆,每一輪
|
||||
# 反覆。一段程式做到冪等,第一次跑就是種入、之後每一次跑都是重建,行為只有一種。
|
||||
# 所以這一支不記「跑過沒有」。要不要動手完全由現況決定:清單上有、待辦簿沒有就加;待辦簿有、
|
||||
# 清單上沒有就移除;兩邊都有就留著不動。第二次跑因此什麼都不會加。
|
||||
#
|
||||
# --- 誰是同一筆:靠 spec_key,不靠 id 也不靠標題 ---
|
||||
#
|
||||
# 待辦簿的 id 是「建立時間加標題」的雜湊,跟技能名沒有關係,所以反查不了。反查鍵是待辦檔裡
|
||||
# 的 spec_key 欄位,值是 jsc-{domain}:{技能名},由 tasks.sh add 的 --spec-key 寫進去。
|
||||
# 完整理由寫在 tasks.sh 的檔頭「spec_key 為什麼非有不可」,這裡只記結論:拿標題當鍵會讓
|
||||
# 標題的措辭變成介面,拿 action 當鍵會讓提醒類的那十幾筆全部撞在一起,也會撞上使用者自己
|
||||
# 交辦、動作剛好是同一支技能的那一筆。
|
||||
#
|
||||
# --- 哪些欄位從清單來、哪些自己補 ---
|
||||
#
|
||||
# 清單有十一欄,待辦簿有十五個鍵,對得上的只有兩欄。逐個交代:
|
||||
# 從清單直接抄過來
|
||||
# trigger 原樣抄。第一次什麼時候到期是判定結果,不是這一支的決定
|
||||
# recur 原樣抄。同上
|
||||
# 從清單推出來
|
||||
# spec_key domain 與 name 兩欄合起來寫成 jsc-{domain}:{技能名},那就是一支技能的身分
|
||||
# action 由 way 推,對映見下一段。way 與 action 不是同一件事
|
||||
# title 固定寫成「委派清單內建項:{spec_key}」。標題刻意不含 way、trigger、recur:
|
||||
# 那幾欄會隨清單改動而變,寫進標題就等於每一次改動都換一個 id,而 id 一換,
|
||||
# last_run 與 fail_count 的歷史就跟著斷掉
|
||||
# 這一支自己補,清單裡沒有對應欄位
|
||||
# kind 一律 check。清單管的是「定期做的事」,交辦事項是使用者當面給的,不從清單來
|
||||
# origin 一律 assistant。這一欄決定重建時動不動它,所以不能空、也不能是 user
|
||||
# repo 一律留空。這幾項是機器層級的檢查,不綁單一存取庫;要掃存取庫的那幾項由助理
|
||||
# 自己掃工作目錄底下所有存取庫,不逐筆綁路徑
|
||||
# due 一律留空。清單沒有截止時間這一欄,補一個猜出來的截止時間會讓監控頁標出一批
|
||||
# 沒有人約定過的逾期。留空是合法狀態,意思是沒有截止時間
|
||||
# 不抄過來的清單欄位
|
||||
# verdict 只用來決定要不要種入,見下面「條件式交的那幾支一律不種入」
|
||||
# slice、human 是給人讀的判定說明,逐字抄進標題會讓標題長到在狀態表上看不完;要看它們
|
||||
# 就去看清單本身
|
||||
# next 跑完之後建議接哪一支,那是建議下一個指令那一項要用的資料,跟排程無關
|
||||
# version、origin 清單自己的稽核欄位。清單的 origin 是 seed 或 judged,講的是「這一列
|
||||
# 判定過沒有」;待辦簿的 origin 是 user 或 assistant,講的是「這一筆誰交辦的」。
|
||||
# 兩個同名不同義,一律不互抄
|
||||
# state、last_run、next_run、fail_count 由 tasks.sh 與判到期那一邊維護,這一支不碰。
|
||||
#
|
||||
# --- way 與 action 的對映 ---
|
||||
#
|
||||
# 清單的 way 是交出方式,三種:invoke 觸發、patrol 巡檢、remind 提醒。待辦簿的 action 是
|
||||
# 助理實際要跑的事,三種:技能名、腳本、remind。兩套詞彙不對應,所以要明寫對映:
|
||||
# way 含 invoke → action 取技能名。觸發的定義就是呼叫既有技能、內容照那支技能自己的
|
||||
# 流程走,所以填技能名就是照判定結果做
|
||||
# 其餘(patrol、remind、patrol,remind)→ action 取 remind
|
||||
# 第二條的理由要講清楚,因為它看起來像偷懶。巡檢那個交出方式的意思是「助理在自己那一輪裡
|
||||
# 順手做那一段唯讀盤點」,而那一段沒有獨立的入口:巡檢那一輪讀的是固定那幾項來源,沒有
|
||||
# 一支腳本或一個技能名代表得了「某一支技能的唯讀切片」。這時候把技能名填進 action,助理下一輪
|
||||
# 就會去呼叫整支技能,那正是切片交要防的事——留在人手上的那一半會被一路跑完。
|
||||
# 所以填 remind:助理照時程提醒該做那一段、指出入口,不動手。等哪一天那些切片各自有了自己的
|
||||
# 入口,改的是這個對映,不是待辦簿的格式。
|
||||
#
|
||||
# --- 條件式交的那幾支一律不種入 ---
|
||||
#
|
||||
# 判定是 cond 的那幾列,預設一筆都不種入,並且逐列印一行 held=,把清單上的條件原文帶出來。
|
||||
# 理由是條件本身是散文,程式判不了。清單裡沒有一個機器讀得懂的條件欄位,只有 slice 與 human
|
||||
# 兩欄的說明文字,所以「條件成立了沒有」這件事現在只有人答得出來。
|
||||
# 預設種入的代價已經有現成的例子:其中一支健檢技能的條件明寫「只有在它自家路徑不再帶版本號
|
||||
# 之後才可以交」,而那個條件現在不成立——真的種進去,助理每一輪都會去叫它,那一輪會停在
|
||||
# 第一支自家腳本上,因為帶版本號的路徑進不了允許清單,而且是無聲的:沒有錯誤、沒有輸出,
|
||||
# 心跳照寫,看起來完全正常。一個會無聲卡死的項目比一個缺掉的項目難查得多。
|
||||
# 預設不種入的代價是「少了它,看不出為什麼」,那個代價用 held= 那幾行補掉:每一列印出技能名、
|
||||
# 判定、條件原文與留在人手上的那一半,摘要另外印 held= 的筆數。少掉的那幾支看得見、也看得出
|
||||
# 是刻意少的,不是漏的。
|
||||
# 人確認過某一支的條件成立了,就帶 --allow-cond jsc-{domain}:{技能名} 種入那一支,一支一支帶,
|
||||
# 不給一個「全部放行」的旗標:全部放行等於用一個決定蓋掉三個各自不同的條件。
|
||||
#
|
||||
# --- 清單改了 trigger、recur 或 way 怎麼辦 ---
|
||||
#
|
||||
# 規格的重建規則只有三條:清單新增可交項目就加一筆、清單移除或改成不交就移除那一筆、
|
||||
# origin=user 的一律不動。一支技能還在清單上、還是可交,只是 trigger、recur 或 way 換了值,
|
||||
# 三條規則一條都沒講到。這一支的處置是:**預設只印 drift= 報出來,不改那一筆。**
|
||||
# 兩個理由。一、tasks.sh 刻意沒有 edit 操作,改欄位值只能移除再重新登錄,而重新登錄會換一個
|
||||
# 新的 id,last_run 與 fail_count 從此歸零——一個原本已經連續失敗五次的項目會看起來像全新的,
|
||||
# 沒有人會知道它壞了。二、規格沒講的事不自己補一條規則進去,尤其是會弄丟資料的那一種。
|
||||
# 印出來,人看得到,要不要換由人決定。
|
||||
# 人決定要換就帶 --refresh:那一筆先 remove 再 add,並且明說歷史會歸零。
|
||||
#
|
||||
# --- 空清單一律不動手 ---
|
||||
#
|
||||
# 清單讀不到(回 1)或讀到了卻解不出任何一列可交項目(回 2),這一支一筆都不移除,直接回報。
|
||||
# 這一條是刻意寫的例外,不是漏掉:一致化的邏輯照字面跑,「清單上沒有」就等於「該移除」,
|
||||
# 於是一個沒裝 jsc-meta 的機器、一個 clone 到一半的存取庫、一個被改壞的清單檔,都會讓這一支
|
||||
# 把所有內建項一次刪光,而且回 0 看起來完全成功。刪光之後助理就再也不做任何定期檢查了,
|
||||
# 而待辦簿上什麼都沒有,看不出曾經有過。
|
||||
# 所以「清單是空的」與「清單說沒有可交項目」在這裡當成兩件事:前者是讀取失敗,一律不動手。
|
||||
#
|
||||
# --- 使用者交辦的那幾筆 ---
|
||||
#
|
||||
# origin=user 的待辦一律不動,這一支連讀都只讀不比對:不加、不移除、不算進 drift。
|
||||
# 擋在兩層。這一支自己跳過那幾筆,而 tasks.sh 的 remove 對 origin=user 一律回 7,除非人親自
|
||||
# 帶 --force——這一支一次都不帶那個旗標。手改過的檔案有可能出現 origin=user 卻帶著 spec_key
|
||||
# 的組合(tasks.sh add 擋得住這個組合,手寫檔案擋不住),那幾筆印一行 skip_user= 並跳過,
|
||||
# 不當成孤兒移除。
|
||||
#
|
||||
# 環境變數:
|
||||
# JSC_HOME 助理狀態檔的根目錄,預設 ~/.jsc。要是連 HOME 也沒有就回 6,不猜
|
||||
# JSC_DELEGATE_SPEC 委派清單的路徑,優先於 --root 與自動搜尋
|
||||
# JSC_TASKS_SH 本 domain 的 tasks.sh 路徑,優先於自動搜尋
|
||||
set -u
|
||||
|
||||
JSC_HOME_RAW="${JSC_HOME:-}"
|
||||
if [ -z "$JSC_HOME_RAW" ]; then
|
||||
JSC_HOME_RAW="${HOME:-}"
|
||||
[ -n "$JSC_HOME_RAW" ] || {
|
||||
printf '[jsc][助理內建項][ERR]:JSC_HOME 與 HOME 都沒有設定,找不到待辦簿的根目錄。這裡不猜一個路徑:猜錯就是拿另一個地方的待辦簿去對清單,於是把該有的那幾筆全部當成缺的再加一次。請設定 JSC_HOME 再跑一次。\n' >&2
|
||||
exit 6
|
||||
}
|
||||
JSC_HOME_RAW="$JSC_HOME_RAW/.jsc"
|
||||
fi
|
||||
case "$JSC_HOME_RAW" in
|
||||
/*) ;;
|
||||
*)
|
||||
_abs=$(CDPATH= cd -- "$JSC_HOME_RAW" 2>/dev/null && pwd -L) || _abs=''
|
||||
[ -n "$_abs" ] || {
|
||||
printf '[jsc][助理內建項][ERR]:JSC_HOME 是相對路徑(%s),也解不出絕對路徑。待辦簿的位置必須是字面絕對路徑,請把 JSC_HOME 設成絕對路徑再跑一次。\n' "$JSC_HOME_RAW" >&2
|
||||
exit 6
|
||||
}
|
||||
JSC_HOME_RAW="$_abs" ;;
|
||||
esac
|
||||
|
||||
JSC_HOME="$JSC_HOME_RAW"
|
||||
CURRENT="$JSC_HOME/current"
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
||||
SCRIPT_DIR="${SCRIPT_DIR:-.}"
|
||||
|
||||
TAB=$(printf '\t')
|
||||
|
||||
die() { # $1=結束碼 $2=訊息
|
||||
printf '[jsc][助理內建項][ERR]:%s\n' "$2" >&2
|
||||
exit "$1"
|
||||
}
|
||||
warn() { printf '[jsc][助理內建項][WARN]:%s\n' "$1" >&2; }
|
||||
note() { printf '[jsc][助理內建項]:%s\n' "$1" >&2; }
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
usage: seed-tasks.sh plan [--root 字面絕對路徑] [--spec 清單檔]
|
||||
[--allow-cond jsc-{domain}:{技能名}]... [--refresh]
|
||||
seed-tasks.sh apply [--root 字面絕對路徑] [--spec 清單檔]
|
||||
[--allow-cond jsc-{domain}:{技能名}]... [--refresh]
|
||||
EOF
|
||||
exit 6
|
||||
}
|
||||
|
||||
# 判準與處置同這個 domain 的其他腳本:只警告、照跑。從工作樹直接跑是開發時的正當用法。
|
||||
warn_if_not_current() {
|
||||
_want="$CURRENT/jsc-assist/tools/$(basename -- "$0")"
|
||||
case "$SCRIPT_DIR/" in
|
||||
"$CURRENT"/*) return 0 ;;
|
||||
esac
|
||||
warn "這支腳本是從 $SCRIPT_DIR/$(basename -- "$0") 跑起來的,不是 $_want。權限閘門只放行 current 那一組確切路徑:無人值守那一輪用別的路徑會被靜靜擋掉。開發時這樣跑沒關係。"
|
||||
return 0
|
||||
}
|
||||
warn_if_not_current
|
||||
|
||||
valid_spec_key() {
|
||||
printf '%s' "$1" | LC_ALL=C grep -qE '^jsc-[a-z0-9-]+:[a-z0-9-]+$'
|
||||
}
|
||||
|
||||
# --- 找委派清單 ---
|
||||
|
||||
# 根目錄優先取 --root 給的字面絕對路徑,理由與技能本文取根目錄的規則相同:無人值守那一輪
|
||||
# 自己不解根目錄,值由裝排程的人寫進條目、再一路餵下來。呼叫端沒給才自己找,找的順序比照
|
||||
# patrol.sh 的 find_tool():先環境變數覆寫,再 current 那一組連結,然後開發用的並排存取庫
|
||||
# 版面,最後已安裝的快取版面。
|
||||
find_spec() {
|
||||
if [ -n "${JSC_DELEGATE_SPEC:-}" ]; then
|
||||
[ -f "$JSC_DELEGATE_SPEC" ] && { printf '%s\n' "$JSC_DELEGATE_SPEC"; return 0; }
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$OPT_SPEC" ]; then
|
||||
[ -f "$OPT_SPEC" ] && { printf '%s\n' "$OPT_SPEC"; return 0; }
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$OPT_ROOT" ]; then
|
||||
for _c in "$OPT_ROOT/jsc-meta/tools/delegate-spec.tsv" "$OPT_ROOT/meta/tools/delegate-spec.tsv"; do
|
||||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||
done
|
||||
return 1
|
||||
fi
|
||||
for _c in "$CURRENT/jsc-meta/tools/delegate-spec.tsv" "$CURRENT/meta/tools/delegate-spec.tsv"; do
|
||||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||
done
|
||||
_root="${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}"
|
||||
for _c in "$_root/../meta/tools/delegate-spec.tsv" "$_root/../jsc-meta/tools/delegate-spec.tsv"; do
|
||||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||
done
|
||||
_c=$(ls -d "$_root"/../../jsc-meta/*/tools/delegate-spec.tsv \
|
||||
"$_root"/../../meta/*/tools/delegate-spec.tsv \
|
||||
"$HOME"/.claude/plugins/cache/*/jsc-meta/*/tools/delegate-spec.tsv 2>/dev/null \
|
||||
| sort | tail -n1)
|
||||
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||
return 1
|
||||
}
|
||||
|
||||
# tasks.sh 一律取這一支腳本旁邊那一份,不去 current 或快取裡另外挑:兩支同 domain 的腳本
|
||||
# 混到不同版本,欄位順序或結束碼一不一樣都看不出來,而寫入的是待辦簿本身。
|
||||
find_tasks_sh() {
|
||||
if [ -n "${JSC_TASKS_SH:-}" ]; then
|
||||
[ -f "$JSC_TASKS_SH" ] && { printf '%s\n' "$JSC_TASKS_SH"; return 0; }
|
||||
return 1
|
||||
fi
|
||||
[ -f "$SCRIPT_DIR/tasks.sh" ] && { printf '%s\n' "$SCRIPT_DIR/tasks.sh"; return 0; }
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- way 對 action ---
|
||||
|
||||
# 對映與理由見檔頭「way 與 action 的對映」。way 是半形逗號隔開的清單,比對時前後各補一個
|
||||
# 逗號,才不會讓 invoke 去命中一個叫別的名字但含有 invoke 這幾個字的交出方式。
|
||||
action_of() { # $1=way $2=spec_key
|
||||
case ",$1," in
|
||||
*,invoke,*) printf '%s' "$2" ;;
|
||||
*) printf 'remind' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- 參數 ---
|
||||
|
||||
MODE="${1:-}"
|
||||
[ -n "$MODE" ] || usage
|
||||
case "$MODE" in
|
||||
plan|apply) ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
shift
|
||||
|
||||
OPT_ROOT=''
|
||||
OPT_SPEC=''
|
||||
OPT_REFRESH=0
|
||||
ALLOW_COND=''
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--root) [ "$#" -ge 2 ] || usage; OPT_ROOT="$2"; shift 2 ;;
|
||||
--spec) [ "$#" -ge 2 ] || usage; OPT_SPEC="$2"; shift 2 ;;
|
||||
--refresh) OPT_REFRESH=1; shift ;;
|
||||
--allow-cond)
|
||||
[ "$#" -ge 2 ] || usage
|
||||
valid_spec_key "$2" || die 6 "--allow-cond「$2」不是 jsc-{domain}:{技能名} 這個形狀。形狀不對的值放行不了任何一支,而那一支會被當成沒放行、靜靜少掉。"
|
||||
ALLOW_COND="$ALLOW_COND $2"
|
||||
shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$OPT_ROOT" in
|
||||
''|/*) ;;
|
||||
*) die 6 "--root 給的是「$OPT_ROOT」,不是絕對路徑。根目錄由呼叫端餵進來,這一支不自己解、也不猜:相對路徑在排程那一輪等於指向排程機制的工作目錄。" ;;
|
||||
esac
|
||||
|
||||
allowed_cond() { # $1=spec_key
|
||||
for _a in $ALLOW_COND; do
|
||||
[ "$_a" = "$1" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- 前置 ---
|
||||
|
||||
SPEC=$(find_spec) || SPEC=''
|
||||
[ -n "$SPEC" ] || die 1 "找不到委派清單(jsc-meta 的 tools/delegate-spec.tsv)。這一支一筆都不動:讀不到清單的時候,「清單上沒有」與「這台機器沒裝 jsc-meta」分不出來,照字面跑會把所有內建項一次刪光。請安裝 jsc-meta,或用 --root 指定工具根目錄、用 --spec 直接指定清單檔。"
|
||||
[ -r "$SPEC" ] || die 1 "委派清單在 $SPEC,可是讀不到。一筆都不動,理由同上。請檢查那個檔案的權限。"
|
||||
|
||||
TASKS_SH=$(find_tasks_sh) || TASKS_SH=''
|
||||
[ -n "$TASKS_SH" ] || die 3 "找不到本 domain 的 tasks.sh(找過 $SCRIPT_DIR)。待辦簿只有那一支寫得出來,這一支不自己動 tasks/ 底下的檔案,所以這一次一筆都不動。"
|
||||
|
||||
TMPD=$(mktemp -d 2>/dev/null) || die 5 '建不出暫存目錄,這一輪算不出要改哪幾筆。'
|
||||
trap 'rm -rf "$TMPD"' EXIT
|
||||
WANT="$TMPD/want.tsv"
|
||||
HAVE="$TMPD/have.tsv"
|
||||
: >"$WANT" 2>/dev/null || die 5 "暫存檔寫不進去:$WANT。"
|
||||
: >"$HAVE" 2>/dev/null || die 5 "暫存檔寫不進去:$HAVE。"
|
||||
|
||||
# --- 讀清單,算出「該有哪幾筆」 ---
|
||||
|
||||
N_HELD=0
|
||||
N_BAD=0
|
||||
# 註解列與空白列跳掉。清單是定位字元分隔,欄位順序見清單自己的檔頭。
|
||||
while IFS="$TAB" read -r c_domain c_name c_verdict c_way c_slice c_human c_trigger c_recur c_rest; do
|
||||
case "$c_domain" in
|
||||
''|'#'*) continue ;;
|
||||
esac
|
||||
[ -n "$c_name" ] || continue
|
||||
_key="jsc-$c_domain:$c_name"
|
||||
case "$c_verdict" in
|
||||
none)
|
||||
# 不交的那幾列本來就不該在待辦簿裡,連 held 都不算:它們是判定過決定不交,不是條件
|
||||
# 還沒成立。清單上沒有它們,重建時對應那一筆就會被移除,那正是規格要的行為。
|
||||
continue ;;
|
||||
full|slice) ;;
|
||||
cond)
|
||||
if allowed_cond "$_key"; then
|
||||
note "$_key 是條件式交,這一次由 --allow-cond 放行,照 full 與 slice 同一套種入。條件成立與否由帶這個旗標的人負責。"
|
||||
else
|
||||
# 放行過的那幾支不算保留:摘要的 held= 要等於「這一次少掉幾支」,把放行的也算進去,
|
||||
# 那個數字就跟上面的 held= 行數對不起來。
|
||||
N_HELD=$((N_HELD + 1))
|
||||
printf 'held=%s verdict=cond way=%s\n' "$_key" "$c_way"
|
||||
printf ' 條件:%s\n' "$c_slice"
|
||||
printf ' 留在人手上:%s\n' "$c_human"
|
||||
continue
|
||||
fi ;;
|
||||
*)
|
||||
N_BAD=$((N_BAD + 1))
|
||||
printf 'bad=%s reason=判定欄的值是「%s」,不在 full、slice、cond、none 四個裡面\n' "$_key" "$c_verdict"
|
||||
continue ;;
|
||||
esac
|
||||
# 清單說可交,trigger 或 recur 卻沒有值,這一筆種不出來:補一個猜出來的時程等於讓助理
|
||||
# 拿一個沒有人同意過的排程去跑。印出來讓人回去補清單。
|
||||
if [ -z "$c_trigger" ] || [ "$c_trigger" = '-' ] || [ -z "$c_recur" ] || [ "$c_recur" = '-' ]; then
|
||||
N_BAD=$((N_BAD + 1))
|
||||
printf 'bad=%s reason=判定是 %s 卻沒有 trigger 或 recur(trigger=%s recur=%s),種不出來\n' \
|
||||
"$_key" "$c_verdict" "${c_trigger:--}" "${c_recur:--}"
|
||||
continue
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$_key" 'check' "委派清單內建項:$_key" "$(action_of "$c_way" "$_key")" \
|
||||
"$c_trigger" "$c_recur" >>"$WANT" 2>/dev/null \
|
||||
|| die 5 "暫存檔寫不進去:$WANT。"
|
||||
done <"$SPEC"
|
||||
|
||||
N_WANT=$(awk 'END{print NR+0}' "$WANT")
|
||||
if [ "$N_WANT" -eq 0 ]; then
|
||||
printf 'mode=%s spec=%s want=0 held=%s bad=%s\n' "$MODE" "$SPEC" "$N_HELD" "$N_BAD"
|
||||
die 2 "委派清單 $SPEC 讀到了,卻一列可交項目都解不出來(保留 $N_HELD 列、對不上 $N_BAD 列)。一筆都不移除:一份被改壞或抄到一半的清單照字面跑會把所有內建項刪光,而那之後助理就不再做任何定期檢查,待辦簿上也看不出曾經有過。請先確認清單本身。"
|
||||
fi
|
||||
|
||||
# --- 讀待辦簿,算出「現在有哪幾筆」 ---
|
||||
|
||||
# list 的欄位順序:id、kind、state、title、action、trigger、recur、repo、due、last_run、
|
||||
# next_run、fail_count、origin、spec_key。這裡只留帶 spec_key 的那幾筆,其餘與清單無關。
|
||||
if ! "$TASKS_SH" list --no-header >"$TMPD/list.tsv" 2>"$TMPD/list.err"; then
|
||||
cat "$TMPD/list.err" >&2
|
||||
die 3 "$TASKS_SH list 回了非 0,讀不出待辦簿現況。一筆都不動:讀不到現況的時候,每一筆都會被當成缺的再加一次。"
|
||||
fi
|
||||
awk -F"$TAB" 'NF>=14 && $14!=""{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",$14,$1,$13,$2,$4,$5,$6,$7}' \
|
||||
"$TMPD/list.tsv" >"$HAVE" 2>/dev/null || die 5 "暫存檔寫不進去:$HAVE。"
|
||||
|
||||
N_SKIP_USER=0
|
||||
N_DUP=0
|
||||
|
||||
# --- 一致化 ---
|
||||
|
||||
N_ADD=0
|
||||
N_REMOVE=0
|
||||
N_KEEP=0
|
||||
N_DRIFT=0
|
||||
RC_PARTIAL=0
|
||||
|
||||
run_add() { # $1=spec_key $2=kind $3=title $4=action $5=trigger $6=recur
|
||||
if [ "$MODE" = plan ]; then
|
||||
printf 'add=%s action=%s trigger=%s recur=%s title=%s(plan,沒有寫進去)\n' "$1" "$4" "$5" "$6" "$3"
|
||||
N_ADD=$((N_ADD + 1))
|
||||
return 0
|
||||
fi
|
||||
# 結束碼要在呼叫的下一行就接住。寫成 if 的條件再到後面讀 $?,讀到的是那個 if 整段的碼,
|
||||
# 失敗那一路永遠是 0,於是每一筆失敗都會被回報成 exit=0。
|
||||
"$TASKS_SH" add --kind "$2" --title "$3" --action "$4" --trigger "$5" \
|
||||
--recur "$6" --origin assistant --spec-key "$1" >"$TMPD/add.out" 2>"$TMPD/add.err"
|
||||
_rc=$?
|
||||
if [ "$_rc" -eq 0 ]; then
|
||||
_id=$(sed -n 's/^added=\([0-9A-Fa-f]*\).*/\1/p' "$TMPD/add.out" | head -n1)
|
||||
printf 'add=%s id=%s action=%s trigger=%s recur=%s\n' "$1" "${_id:--}" "$4" "$5" "$6"
|
||||
N_ADD=$((N_ADD + 1))
|
||||
return 0
|
||||
fi
|
||||
cat "$TMPD/add.err" >&2
|
||||
printf 'add_failed=%s exit=%s\n' "$1" "$_rc"
|
||||
RC_PARTIAL=1
|
||||
return 1
|
||||
}
|
||||
|
||||
run_remove() { # $1=spec_key $2=id $3=理由
|
||||
if [ "$MODE" = plan ]; then
|
||||
printf 'remove=%s id=%s reason=%s(plan,沒有刪掉)\n' "$1" "$2" "$3"
|
||||
N_REMOVE=$((N_REMOVE + 1))
|
||||
return 0
|
||||
fi
|
||||
# 一次都不帶 --force:那個旗標留給人。無人值守那一輪碰到 origin=user 就該被擋下。
|
||||
"$TASKS_SH" remove "$2" >"$TMPD/rm.out" 2>"$TMPD/rm.err"
|
||||
_rc=$?
|
||||
if [ "$_rc" -eq 0 ]; then
|
||||
printf 'remove=%s id=%s reason=%s\n' "$1" "$2" "$3"
|
||||
N_REMOVE=$((N_REMOVE + 1))
|
||||
return 0
|
||||
fi
|
||||
cat "$TMPD/rm.err" >&2
|
||||
printf 'remove_failed=%s id=%s exit=%s\n' "$1" "$2" "$_rc"
|
||||
RC_PARTIAL=1
|
||||
return 1
|
||||
}
|
||||
|
||||
# 一、清單上有的,待辦簿裡有沒有。
|
||||
while IFS="$TAB" read -r w_key w_kind w_title w_action w_trigger w_recur; do
|
||||
_hits=$(awk -F"$TAB" -v k="$w_key" '$1==k{n++} END{print n+0}' "$HAVE")
|
||||
if [ "$_hits" -eq 0 ]; then
|
||||
run_add "$w_key" "$w_kind" "$w_title" "$w_action" "$w_trigger" "$w_recur" || true
|
||||
continue
|
||||
fi
|
||||
if [ "$_hits" -gt 1 ]; then
|
||||
# 同一個鍵有兩筆以上,這一支不猜該留哪一筆:兩筆的 last_run 與 fail_count 不同,刪錯
|
||||
# 的那一筆的歷史就沒了。印出來讓人挑。
|
||||
N_DUP=$((N_DUP + 1))
|
||||
printf 'dup=%s count=%s reason=同一個清單鍵有多筆,這一支不動它,請人留一筆\n' "$w_key" "$_hits"
|
||||
awk -F"$TAB" -v k="$w_key" '$1==k{printf " dup_id=%s origin=%s title=%s\n",$2,$3,$5}' "$HAVE"
|
||||
continue
|
||||
fi
|
||||
_id=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $2}' "$HAVE")
|
||||
_origin=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $3}' "$HAVE")
|
||||
if [ "$_origin" != assistant ]; then
|
||||
# 手改過的檔案才會出現這個組合,tasks.sh add 擋得住。一律不動,也不算孤兒。
|
||||
N_SKIP_USER=$((N_SKIP_USER + 1))
|
||||
printf 'skip_user=%s id=%s origin=%s reason=帶清單鍵但不是助理內建,一律不動\n' "$w_key" "$_id" "$_origin"
|
||||
continue
|
||||
fi
|
||||
_h_kind=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $4}' "$HAVE")
|
||||
_h_action=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $6}' "$HAVE")
|
||||
_h_trigger=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $7}' "$HAVE")
|
||||
_h_recur=$(awk -F"$TAB" -v k="$w_key" '$1==k{print $8}' "$HAVE")
|
||||
_diff=''
|
||||
[ "$_h_kind" = "$w_kind" ] || _diff="$_diff kind:$_h_kind → $w_kind"
|
||||
[ "$_h_action" = "$w_action" ] || _diff="$_diff action:$_h_action → $w_action"
|
||||
[ "$_h_trigger" = "$w_trigger" ] || _diff="$_diff trigger:$_h_trigger → $w_trigger"
|
||||
[ "$_h_recur" = "$w_recur" ] || _diff="$_diff recur:$_h_recur → $w_recur"
|
||||
if [ -z "$_diff" ]; then
|
||||
printf 'keep=%s id=%s\n' "$w_key" "$_id"
|
||||
N_KEEP=$((N_KEEP + 1))
|
||||
continue
|
||||
fi
|
||||
N_DRIFT=$((N_DRIFT + 1))
|
||||
if [ "$OPT_REFRESH" -eq 1 ]; then
|
||||
printf 'drift=%s id=%s refresh=1 diff=%s\n' "$w_key" "$_id" "$_diff"
|
||||
if run_remove "$w_key" "$_id" '清單的判定結果換了值,--refresh 要求重新登錄'; then
|
||||
run_add "$w_key" "$w_kind" "$w_title" "$w_action" "$w_trigger" "$w_recur" || true
|
||||
fi
|
||||
else
|
||||
printf 'drift=%s id=%s refresh=0 diff=%s\n' "$w_key" "$_id" "$_diff"
|
||||
printf ' 這一筆沒有改。要照清單換值請帶 --refresh,那一筆會先移除再重新登錄,id 會換,last_run 與 fail_count 會歸零。\n'
|
||||
N_KEEP=$((N_KEEP + 1))
|
||||
fi
|
||||
done <"$WANT"
|
||||
|
||||
# 二、待辦簿裡有、清單上沒有的,就是孤兒。
|
||||
while IFS="$TAB" read -r h_key h_id h_origin h_kind h_title h_action h_trigger h_recur; do
|
||||
[ -n "$h_key" ] || continue
|
||||
if awk -F"$TAB" -v k="$h_key" '$1==k{f=1} END{exit f?0:1}' "$WANT"; then
|
||||
continue
|
||||
fi
|
||||
if [ "$h_origin" != assistant ]; then
|
||||
N_SKIP_USER=$((N_SKIP_USER + 1))
|
||||
printf 'skip_user=%s id=%s origin=%s reason=帶清單鍵但不是助理內建,一律不動\n' "$h_key" "$h_id" "$h_origin"
|
||||
continue
|
||||
fi
|
||||
run_remove "$h_key" "$h_id" '清單上已經沒有這一支,或它改判成不交' || true
|
||||
done <"$HAVE"
|
||||
|
||||
# --- 摘要 ---
|
||||
|
||||
N_HAVE=$(awk 'END{print NR+0}' "$HAVE")
|
||||
printf 'mode=%s spec=%s tasks_sh=%s want=%s have=%s added=%s removed=%s kept=%s drift=%s held=%s bad=%s dup=%s skip_user=%s\n' \
|
||||
"$MODE" "$SPEC" "$TASKS_SH" "$N_WANT" "$N_HAVE" "$N_ADD" "$N_REMOVE" "$N_KEEP" \
|
||||
"$N_DRIFT" "$N_HELD" "$N_BAD" "$N_DUP" "$N_SKIP_USER"
|
||||
|
||||
[ "$N_HELD" -gt 0 ] && note "有 $N_HELD 支是條件式交,這一次沒有種入,逐支印在上面的 held= 那幾行。條件是散文,程式判不了;人確認過某一支的條件成立就帶 --allow-cond 那一支的鍵。"
|
||||
[ "$N_BAD" -gt 0 ] && warn "清單上有 $N_BAD 列對不上,那幾支這一次沒有種入,逐列印在上面的 bad= 那幾行。請回去補清單,不要在這裡補預設值。"
|
||||
[ "$N_DRIFT" -gt 0 ] && [ "$OPT_REFRESH" -eq 0 ] && note "有 $N_DRIFT 筆的判定結果與清單不一樣,這一次照原樣留著。要換值請帶 --refresh,並且知道那一筆的 last_run 與 fail_count 會歸零。"
|
||||
|
||||
[ "$RC_PARTIAL" -eq 0 ] || die 4 "有 add 或 remove 失敗,其餘各筆照做完了。失敗的逐筆印在上面的 add_failed= 與 remove_failed= 那幾行,各自帶了 tasks.sh 的結束碼,照那一支的結束碼表處理。"
|
||||
exit 0
|
||||
+123
-20
@@ -7,24 +7,29 @@
|
||||
# tasks.sh add --kind {check|todo} --title {一句話} --action {技能|腳本|remind}
|
||||
# --trigger {at:...|after:...} --recur {once|every:...|cron:...}
|
||||
# --origin {user|assistant} [--repo {存取庫}] [--due {ISO 時間}]
|
||||
# [--dry-run]
|
||||
# [--spec-key jsc-{domain}:{技能名}] [--dry-run]
|
||||
# tasks.sh done {id} [--last-run {ISO 時間}] [--next-run {ISO 時間}]
|
||||
# tasks.sh fail {id} [--last-run {ISO 時間}]
|
||||
# tasks.sh pause {id}
|
||||
# tasks.sh resume {id}
|
||||
# tasks.sh remove {id} [--force]
|
||||
#
|
||||
# 結束碼:
|
||||
# 0 成功。list 印完(零筆也算成功);add 寫成一筆;done、fail、pause、resume 改成了。
|
||||
# 0 成功。list 印完(零筆也算成功);add 寫成一筆;done、fail、pause、resume 改成了;
|
||||
# remove 把那一個檔案刪掉了。
|
||||
# pause 對已經是 paused 的那一筆、resume 對已經是 pending 的那一筆,照樣回 0:同一個
|
||||
# 狀態不算轉移,擋它只會讓呼叫端為了「本來就對」的結果去分流
|
||||
# 1 指名的那一筆不存在:done、fail、pause、resume 給的 id 找不到對應檔案
|
||||
# 1 指名的那一筆不存在:done、fail、pause、resume、remove 給的 id 找不到對應檔案
|
||||
# 2 欄位值不合法:必填欄位缺、值不在允許集合、事件名不在固定詞彙表、標題折完是空的、
|
||||
# id 不是十六進位、fail_count 不是非負整數
|
||||
# id 不是十六進位、fail_count 不是非負整數、spec_key 不是 jsc-{domain}:{技能名} 這個形狀,
|
||||
# 或 spec_key 配上 origin=user
|
||||
# 3 不合法的狀態轉移,已擋下。哪些合法見下面「狀態怎麼轉」那張表
|
||||
# 4 這一筆已經有了:add 算出來的完整雜湊撞上一個「建立時間與標題都相同」的既有檔案
|
||||
# 5 檔案系統或雜湊失敗:待辦簿目錄建不起來、檔案寫不進去、這台機器算不出 SHA-1
|
||||
# 6 用法錯誤:不認得的子命令、不認得的選項、選項缺值、缺 id,或 JSC_HOME 與 HOME 都
|
||||
# 解不出絕對路徑(沒有根目錄可寫,猜一個等於把待辦簿寫到別的地方去)
|
||||
# 7 remove 擋下:那一筆的 origin 是 user,而這一次沒有帶 --force。理由見下面
|
||||
# 「remove 為什麼要擋使用者交辦的那幾筆」
|
||||
#
|
||||
# --- 這一支負責什麼、不負責什麼 ---
|
||||
#
|
||||
@@ -53,11 +58,11 @@
|
||||
#
|
||||
# --- 存放格式:純文字 key=value,一行一欄位 ---
|
||||
#
|
||||
# 一筆固定十四個鍵,順序固定,缺一個都不寫。十四個裡有兩個是格式自己需要的:
|
||||
# 一筆固定十五個鍵,順序固定,缺一個都不寫。十五個裡有兩個是格式自己需要的:
|
||||
# id 檔名,也寫進檔案裡一份。只看檔名的話,檔案被複製或改名之後就對不上內容
|
||||
# created 建立時間,UTC 的 ISO 時間。id 是由它與 title 算出來的,不存它就再也算不回
|
||||
# 同一個 id,也就驗不出檔名對不對,碰撞時也接不下去
|
||||
# 其餘十二個是待辦本身的欄位:
|
||||
# 其餘十三個是待辦本身的欄位:
|
||||
# kind check(定期檢查項)或 todo(交辦事項)。同一本簿、同一組欄位,只用它分
|
||||
# title 一句話講完要做什麼
|
||||
# action 助理實際要跑的事:技能名、腳本,或 remind(只提醒,不動手)
|
||||
@@ -70,11 +75,30 @@
|
||||
# next_run 下一次預定執行的時間
|
||||
# fail_count 連續失敗次數
|
||||
# origin user(使用者交辦)或 assistant(助理內建)
|
||||
# spec_key 這一筆是哪一支技能的內建項,寫成 jsc-{domain}:{技能名}。使用者交辦的一律空
|
||||
#
|
||||
# 值是空的照樣把那一行寫出來(例如 repo=)。空值有明確的意思——沒有綁存取庫、沒有截止
|
||||
# 時間、還沒跑過——所以讓每一筆的形狀都一樣,讀的人不必去分「鍵不見了」與「鍵在但是空的」,
|
||||
# 兩眼一比就看得出哪一欄沒填。不認得的鍵一律忽略,往後加欄位不會讓舊檔案讀不進來。
|
||||
#
|
||||
# --- spec_key 為什麼非有不可 ---
|
||||
#
|
||||
# 助理的內建檢查項是照委派清單種進來的,清單改了就要重建:清單新增一支就加一筆,一支改成
|
||||
# 不交或整列被刪掉就把對應那一筆移除。所以重建那一邊一定要能從「一支技能」反查到「待辦簿裡
|
||||
# 屬於它的那一筆」,而其他每一個欄位都反查不了:
|
||||
# id 是「建立時間加標題」的雜湊,跟技能名沒有關係,算不回來也查不過去
|
||||
# title 標題是給人看的一句話。拿它當鍵,等於把標題的措辭變成介面:改一個字,舊那一筆
|
||||
# 就再也認不出來,於是每次重建都刪不掉舊的、又加一筆新的,同一個檢查每輪做兩次
|
||||
# action 交出方式是提醒的那幾筆,action 全部都是 remind 這個同一個字,一支都分不出來;
|
||||
# 而交出方式是觸發的那幾筆,action 是技能名,會跟使用者自己交辦、動作剛好也是
|
||||
# 那一支技能的那一筆撞在一起——撞上就會把使用者交辦的事當成內建項刪掉
|
||||
# origin 只分得出「助理內建」與「使用者交辦」兩群,群裡是哪一支分不出來
|
||||
# 所以身分要有自己的一欄。spec_key 只放身分,不放判定結果:清單裡的 trigger、recur、way
|
||||
# 各自對映到別的欄位,那幾欄會隨清單改動而變,身分不會。
|
||||
# 這一欄與 origin 是兩件事,不可以互相推導:origin=assistant 而 spec_key 是空的,代表這一筆
|
||||
# 是助理自己因為別的理由建的、不受清單管;origin=user 而帶 spec_key 一律擋下(回 2),
|
||||
# 不然下一次重建就會拿清單去刪使用者交辦的事,而規格明寫那幾筆一律不動。
|
||||
#
|
||||
# --- 值裡有等號或換行怎麼辦 ---
|
||||
#
|
||||
# 兩條約定,合起來讓這個格式壞不了,而且不必發明跳脫規則:
|
||||
@@ -127,6 +151,8 @@
|
||||
# paused resume pending paused 只由人設,也只有人解得開
|
||||
# pending resume pending(不算轉移,回 0)
|
||||
# paused pause paused(不算轉移,回 0)
|
||||
# 任何狀態 remove (不存在) 這一筆整個不見。三個狀態都收,理由見下面
|
||||
# 「remove 為什麼要擋使用者交辦的那幾筆」
|
||||
# 被擋下的幾條,各自的理由:
|
||||
# paused + done 停掉的那一筆助理本來就沒有在跑,標成做完等於偷偷把它解開又收掉。要收
|
||||
# 先 resume,讓「解開」這件事是人做的、看得到的
|
||||
@@ -137,9 +163,9 @@
|
||||
# 與 fail。失敗連續幾次都一樣留在 pending,靠 fail_count 讓人看到,不自動停掉——自動停掉
|
||||
# 等於助理自己決定不做某件事,而且沒有人會發現。
|
||||
#
|
||||
# --- 為什麼是六個操作,不是四個 ---
|
||||
# --- 為什麼是七個操作,不是四個 ---
|
||||
#
|
||||
# 存放層要的是四個:list、add、done、pause。另外兩個是補洞,不是加功能:
|
||||
# 存放層要的是四個:list、add、done、pause。另外三個是補洞,不是加功能:
|
||||
# fail last_run、next_run、fail_count 三個欄位由助理自己維護、不由人填,但四個操作裡
|
||||
# 沒有一個寫得到 fail_count。少了它,fail_count 永遠是 0,監控頁與提醒上的
|
||||
# 「已連續失敗 N 次」就永遠是 0 次,於是一個壞掉的項目每輪重試而沒有人知道——
|
||||
@@ -147,11 +173,30 @@
|
||||
# resume paused 只由人設,也就只有人解得開,沒有別的元件寫得出這個轉移。只給 pause
|
||||
# 不給 resume,pause 就是一道單向門:停掉的那一筆再也回不來,人只能去手改檔案,
|
||||
# 而手改檔案繞過了上面那張轉移表。
|
||||
# remove 規格要求「清單移除或改成不交,待辦簿移除對應那筆,不留孤兒」,而六個操作裡沒有
|
||||
# 一個刪得掉一筆。少了它,重建那一邊只剩兩條路:把孤兒留著,於是助理會去跑一支
|
||||
# 判過不交、甚至已經被刪掉的技能,失敗還不會自動暫停,一路重試;或者自己去 rm
|
||||
# 那個檔案,而這一支的檔頭明寫它是唯一寫得出待辦檔的入口,繞過去之後上面那張
|
||||
# 轉移表與碰撞規則就只約束得到一半的寫入者。所以刪除要走同一個入口。
|
||||
# last_run 與 next_run 不另開操作:done 與 fail 都吃 --last-run 與 --next-run,值由呼叫端
|
||||
# 算好餵進來。這一支不算下一次是什麼時候,算的邏輯在別的地方,兩邊各算一次就會漂移。
|
||||
# 沒有 edit 操作。改欄位值要重新登錄一筆,理由是 id 由 created 與 title 算出來,改掉標題
|
||||
# 之後 id 就對不回去了,留一個算不回來的 id 比多一筆待辦糟。
|
||||
#
|
||||
# --- remove 為什麼要擋使用者交辦的那幾筆 ---
|
||||
#
|
||||
# remove 是這一支唯一真的會弄丟資料的操作,而它的主要呼叫端是無人值守那一輪的清單重建。
|
||||
# 規格對那一輪的規定只有一條:origin=user 的項目一律不動——助理不會因為一支技能改判就把
|
||||
# 使用者交辦的事刪掉。那條規定寫在呼叫端,可是刪除只有這一個入口,所以擋在這裡最省:
|
||||
# 呼叫端每多一個,那條規定就要各自再實作一次,漏掉一個就刪掉一件沒有人同意刪的事,而且是
|
||||
# 在沒有人看的時候刪的,事後也查不出來是誰刪的。
|
||||
# 擋法是回 7 並且什麼都不動,不是靜靜跳過:靜靜跳過會讓呼叫端以為刪掉了,於是它下一步就
|
||||
# 去補一筆新的,最後兩筆並存。
|
||||
# --force 留給人:使用者要刪自己交辦的那一筆是正當的,那一次有人在現場。無人值守那一輪
|
||||
# 一律不帶這個旗標。
|
||||
# 三個狀態都收得下 remove,包含 paused:那不是狀態轉移,是這一筆整個不再存在。擋 paused
|
||||
# 反而會讓一支已經刪掉的技能留下一筆永遠刪不掉的孤兒,只因為有人先按了暫停。
|
||||
#
|
||||
# 環境變數:
|
||||
# JSC_HOME 助理狀態檔的根目錄,預設 ~/.jsc。要是連 HOME 也沒有就回 6,不猜
|
||||
# JSC_HASH_ID 共用 hash 規則那一支的路徑,優先於自動搜尋
|
||||
@@ -203,14 +248,17 @@ warn() { printf '[jsc][助理待辦簿][WARN]:%s\n' "$1" >&2; }
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
usage: tasks.sh list [--kind check|todo] [--state pending|done|paused] [--repo 存取庫] [--no-header]
|
||||
usage: tasks.sh list [--kind check|todo] [--state pending|done|paused] [--repo 存取庫]
|
||||
[--spec-key jsc-{domain}:{技能名}] [--no-header]
|
||||
tasks.sh add --kind check|todo --title 一句話 --action 技能|腳本|remind
|
||||
--trigger at:...|after:... --recur once|every:...|cron:...
|
||||
--origin user|assistant [--repo 存取庫] [--due ISO 時間] [--dry-run]
|
||||
--origin user|assistant [--repo 存取庫] [--due ISO 時間]
|
||||
[--spec-key jsc-{domain}:{技能名}] [--dry-run]
|
||||
tasks.sh done {id} [--last-run ISO 時間] [--next-run ISO 時間]
|
||||
tasks.sh fail {id} [--last-run ISO 時間]
|
||||
tasks.sh pause {id}
|
||||
tasks.sh resume {id}
|
||||
tasks.sh remove {id} [--force]
|
||||
EOF
|
||||
exit 6
|
||||
}
|
||||
@@ -284,6 +332,15 @@ valid_trigger() { # $1=trigger
|
||||
|
||||
valid_recur() { case "$1" in once|every:?*|cron:?*) return 0 ;; esac; return 1; }
|
||||
|
||||
# spec_key 是重建內建項時的反查鍵,形狀固定 jsc-{domain}:{技能名},兩段都不得為空。
|
||||
# 收得寬一點的話,一個打錯的鍵會變成一筆永遠對不上清單的孤兒:重建那一邊查不到它,
|
||||
# 所以既不會更新也不會移除,而它看起來跟一筆正常的內建項一模一樣。
|
||||
# 兩段都只收小寫英數與連字號:清單的前兩欄本來就長這樣,而冒號是分隔符號,值裡再出現一個
|
||||
# 就切不回兩段。
|
||||
valid_spec_key() {
|
||||
printf '%s' "$1" | LC_ALL=C grep -qE '^jsc-[a-z0-9-]+:[a-z0-9-]+$'
|
||||
}
|
||||
|
||||
valid_count() { case "$1" in ''|*[!0-9]*) return 1 ;; esac; return 0; }
|
||||
|
||||
# id 直接拿去接檔名,所以只收十六進位。帶斜線或點號開頭的值會把讀寫指到待辦簿目錄外面去。
|
||||
@@ -350,7 +407,7 @@ now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
|
||||
F_id=''; F_created=''; F_kind=''; F_title=''; F_action=''; F_trigger=''
|
||||
F_recur=''; F_repo=''; F_due=''; F_state=''; F_last_run=''; F_next_run=''
|
||||
F_fail_count=''; F_origin=''
|
||||
F_fail_count=''; F_origin=''; F_spec_key=''
|
||||
|
||||
load_record() { # $1=檔案
|
||||
F_id=$(kv_get "$1" id)
|
||||
@@ -367,6 +424,8 @@ load_record() { # $1=檔案
|
||||
F_next_run=$(kv_get "$1" next_run)
|
||||
F_fail_count=$(kv_get "$1" fail_count)
|
||||
F_origin=$(kv_get "$1" origin)
|
||||
# 舊檔案沒有這一鍵,讀回來就是空的,那正好是「不受清單管」的意思,不必補預設值也不必轉檔。
|
||||
F_spec_key=$(kv_get "$1" spec_key)
|
||||
# 手改過的檔案有可能把計數寫成別的東西。當成 0 再往上加,而不是讓算式整支炸掉:這一筆
|
||||
# 的計數本來就已經不可信,讓它從 0 重新開始算得出來,比整支停下更有用。
|
||||
if ! valid_count "$F_fail_count"; then
|
||||
@@ -396,6 +455,7 @@ write_record() { # $1=目標檔案
|
||||
printf 'next_run=%s\n' "$F_next_run"
|
||||
printf 'fail_count=%s\n' "$F_fail_count"
|
||||
printf 'origin=%s\n' "$F_origin"
|
||||
printf 'spec_key=%s\n' "$F_spec_key"
|
||||
} >"$_tmp" 2>/dev/null || { rm -f "$_tmp"; die 5 "待辦簿寫不進去:$_tmp。請確認 $TASKS_DIR 可寫。"; }
|
||||
mv "$_tmp" "$1" 2>/dev/null || { rm -f "$_tmp"; die 5 "待辦簿換不上去:$1。請確認 $TASKS_DIR 可寫。"; }
|
||||
}
|
||||
@@ -418,8 +478,9 @@ resolve_record() { # $1=id;設好 RECORD_FILE
|
||||
|
||||
# 印出改完之後的那一筆,一行講完。改了什麼要看得到,不然呼叫端只拿到一個結束碼。
|
||||
print_record_line() {
|
||||
printf 'id=%s state=%s recur=%s last_run=%s next_run=%s fail_count=%s title=%s\n' \
|
||||
"$F_id" "$F_state" "$F_recur" "${F_last_run:--}" "${F_next_run:--}" "$F_fail_count" "$F_title"
|
||||
printf 'id=%s state=%s recur=%s last_run=%s next_run=%s fail_count=%s spec_key=%s title=%s\n' \
|
||||
"$F_id" "$F_state" "$F_recur" "${F_last_run:--}" "${F_next_run:--}" "$F_fail_count" \
|
||||
"${F_spec_key:--}" "$F_title"
|
||||
}
|
||||
|
||||
# --- list ---
|
||||
@@ -428,20 +489,24 @@ print_record_line() {
|
||||
# 不必再發明引號規則。空欄位就是空的一欄,不填占位符號:填了占位符號,讀的人得再去分
|
||||
# 「真的空」與「占位符號本身」。
|
||||
cmd_list() {
|
||||
_f_kind=''; _f_state=''; _f_repo=''; _header=1
|
||||
_f_kind=''; _f_state=''; _f_repo=''; _f_spec=''; _header=1
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--kind) [ "$#" -ge 2 ] || usage; _f_kind="$2"; shift 2 ;;
|
||||
--state) [ "$#" -ge 2 ] || usage; _f_state="$2"; shift 2 ;;
|
||||
--repo) [ "$#" -ge 2 ] || usage; _f_repo="$2"; shift 2 ;;
|
||||
--spec-key) [ "$#" -ge 2 ] || usage; _f_spec="$2"; shift 2 ;;
|
||||
--no-header) _header=0; shift ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
[ -z "$_f_kind" ] || valid_kind "$_f_kind" || die 2 "--kind 只收 check 或 todo,給的是「$_f_kind」。"
|
||||
[ -z "$_f_state" ] || valid_state "$_f_state" || die 2 "--state 只收 pending、done 或 paused,給的是「$_f_state」。"
|
||||
# 這個篩選是重建內建項時的反查入口,所以形狀擋在這裡:一個打錯的鍵篩出零筆,跟「這一支
|
||||
# 還沒種進來」的結果一模一樣,而後者會讓呼叫端再補一筆。
|
||||
[ -z "$_f_spec" ] || valid_spec_key "$_f_spec" || die 2 "--spec-key「$_f_spec」不是 jsc-{domain}:{技能名} 這個形狀。打錯的鍵篩出零筆,跟「還沒種進來」看起來一樣,而那會讓呼叫端多加一筆。"
|
||||
|
||||
[ "$_header" -eq 1 ] && printf 'id\tkind\tstate\ttitle\taction\ttrigger\trecur\trepo\tdue\tlast_run\tnext_run\tfail_count\torigin\n'
|
||||
[ "$_header" -eq 1 ] && printf 'id\tkind\tstate\ttitle\taction\ttrigger\trecur\trepo\tdue\tlast_run\tnext_run\tfail_count\torigin\tspec_key\n'
|
||||
|
||||
# 目錄不存在或零筆都算正常結束:助理還沒收過任何一筆待辦,不是失敗。
|
||||
if [ ! -d "$TASKS_DIR" ]; then
|
||||
@@ -459,16 +524,17 @@ cmd_list() {
|
||||
[ -z "$_f_kind" ] || [ "$_f_kind" = "$F_kind" ] || continue
|
||||
[ -z "$_f_state" ] || [ "$_f_state" = "$F_state" ] || continue
|
||||
[ -z "$_f_repo" ] || [ "$_f_repo" = "$F_repo" ] || continue
|
||||
[ -z "$_f_spec" ] || [ "$_f_spec" = "$F_spec_key" ] || continue
|
||||
case "$F_state" in
|
||||
pending) _rank=0 ;;
|
||||
paused) _rank=1 ;;
|
||||
*) _rank=2 ;;
|
||||
esac
|
||||
_nrk="$F_next_run"; [ -n "$_nrk" ] || _nrk='~'
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$_rank" "$_nrk" "$F_id" \
|
||||
"$F_id" "$F_kind" "$F_state" "$F_title" "$F_action" "$F_trigger" "$F_recur" \
|
||||
"$F_repo" "$F_due" "$F_last_run" "$F_next_run" "$F_fail_count" "$F_origin"
|
||||
"$F_repo" "$F_due" "$F_last_run" "$F_next_run" "$F_fail_count" "$F_origin" "$F_spec_key"
|
||||
_n=$((_n + 1))
|
||||
done | LC_ALL=C sort -t"$(printf '\t')" -k1,1 -k2,2 -k3,3 | cut -f4-
|
||||
|
||||
@@ -480,6 +546,7 @@ cmd_list() {
|
||||
[ -z "$_f_kind" ] || [ "$_f_kind" = "$F_kind" ] || continue
|
||||
[ -z "$_f_state" ] || [ "$_f_state" = "$F_state" ] || continue
|
||||
[ -z "$_f_repo" ] || [ "$_f_repo" = "$F_repo" ] || continue
|
||||
[ -z "$_f_spec" ] || [ "$_f_spec" = "$F_spec_key" ] || continue
|
||||
_n=$((_n + 1))
|
||||
done
|
||||
printf 'count=%s tasks_dir=%s exists=yes\n' "$_n" "$TASKS_DIR" >&2
|
||||
@@ -490,9 +557,10 @@ cmd_list() {
|
||||
|
||||
cmd_add() {
|
||||
_kind=''; _title=''; _action=''; _trigger=''; _recur=''; _origin=''
|
||||
_repo=''; _due=''; _dry=0
|
||||
_repo=''; _due=''; _spec=''; _dry=0
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--spec-key) [ "$#" -ge 2 ] || usage; _spec="$2"; shift 2 ;;
|
||||
--kind) [ "$#" -ge 2 ] || usage; _kind="$2"; shift 2 ;;
|
||||
--title) [ "$#" -ge 2 ] || usage; _title="$2"; shift 2 ;;
|
||||
--action) [ "$#" -ge 2 ] || usage; _action="$2"; shift 2 ;;
|
||||
@@ -516,6 +584,7 @@ cmd_add() {
|
||||
_origin=$(fold_and_warn origin "$_origin")
|
||||
_repo=$(fold_and_warn repo "$_repo")
|
||||
_due=$(fold_and_warn due "$_due")
|
||||
_spec=$(fold_and_warn spec_key "$_spec")
|
||||
|
||||
[ -n "$_kind" ] || die 2 '缺 --kind。'
|
||||
valid_kind "$_kind" || die 2 "--kind 只收 check(定期檢查項)或 todo(交辦事項),給的是「$_kind」。"
|
||||
@@ -527,6 +596,10 @@ cmd_add() {
|
||||
valid_recur "$_recur" || die 2 "--recur「$_recur」不合法。只收 once、every:{間隔} 或 cron:{式子}。trigger 與 recur 是兩個獨立欄位,四種組合都成立,不要壓成兩種。"
|
||||
[ -n "$_origin" ] || die 2 '缺 --origin。user(使用者交辦)或 assistant(助理內建)。清單重建時只動 assistant 那幾筆,所以這一欄不能空。'
|
||||
valid_origin "$_origin" || die 2 "--origin 只收 user 或 assistant,給的是「$_origin」。"
|
||||
if [ -n "$_spec" ]; then
|
||||
valid_spec_key "$_spec" || die 2 "--spec-key「$_spec」不是 jsc-{domain}:{技能名} 這個形狀。形狀不對的鍵在重建時對不上清單,於是那一筆既不會更新也不會移除,而它看起來跟一筆正常的內建項一樣。"
|
||||
[ "$_origin" = assistant ] || die 2 "--spec-key 只能配 --origin assistant,這一次的 origin 是「$_origin」。使用者交辦的那一筆帶上清單鍵,下一次重建就會拿清單去刪它,而規格明寫 origin=user 的項目一律不動。要照清單種入請用 assistant;要記下是為了哪一支技能交辦的,請寫進標題。"
|
||||
fi
|
||||
|
||||
_created=$(now_iso)
|
||||
# 雜湊吃的是「建立時間加標題」,中間夾一個定位字元當分隔。標題已經折過,裡面不會有定位
|
||||
@@ -566,13 +639,15 @@ cmd_add() {
|
||||
# 一律生在 pending。生在 done 的那一筆是噪音,生在 paused 是事後才會有的人為決定。
|
||||
F_state=pending
|
||||
F_last_run=''; F_next_run=''; F_fail_count=0; F_origin="$_origin"
|
||||
F_spec_key="$_spec"
|
||||
|
||||
if [ "$_dry" -eq 1 ]; then
|
||||
printf 'dryrun=add id=%s file=%s hash40=%s prefix_len=%s\n' "$_id" "$TASKS_DIR/$_id" "$_full" "$_len"
|
||||
printf -- '--- 會寫進去的內容 ---\n'
|
||||
printf 'id=%s\ncreated=%s\nkind=%s\ntitle=%s\naction=%s\ntrigger=%s\nrecur=%s\nrepo=%s\ndue=%s\nstate=%s\nlast_run=%s\nnext_run=%s\nfail_count=%s\norigin=%s\n' \
|
||||
printf 'id=%s\ncreated=%s\nkind=%s\ntitle=%s\naction=%s\ntrigger=%s\nrecur=%s\nrepo=%s\ndue=%s\nstate=%s\nlast_run=%s\nnext_run=%s\nfail_count=%s\norigin=%s\nspec_key=%s\n' \
|
||||
"$F_id" "$F_created" "$F_kind" "$F_title" "$F_action" "$F_trigger" "$F_recur" \
|
||||
"$F_repo" "$F_due" "$F_state" "$F_last_run" "$F_next_run" "$F_fail_count" "$F_origin"
|
||||
"$F_repo" "$F_due" "$F_state" "$F_last_run" "$F_next_run" "$F_fail_count" "$F_origin" \
|
||||
"$F_spec_key"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -717,6 +792,33 @@ cmd_resume() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- remove ---
|
||||
|
||||
cmd_remove() {
|
||||
_id="${1:-}"; [ -n "$_id" ] || usage; shift
|
||||
_force=0
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--force) _force=1; shift ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
open_target "$_id"
|
||||
# 擋在這裡而不擋在呼叫端,理由見檔頭「remove 為什麼要擋使用者交辦的那幾筆」。
|
||||
if [ "$F_origin" = user ] && [ "$_force" -eq 0 ]; then
|
||||
die 7 "id=$F_id 的 origin 是 user,這是使用者交辦的事,remove 不動它。清單重建那一輪一律不帶 --force:助理不會因為一支技能改判就把使用者交辦的事刪掉。要真的刪請人親自帶 --force 再跑一次。"
|
||||
fi
|
||||
# 先把內容留在畫面上再刪。刪掉之後那一筆的欄位就再也拿不回來了,回報裡只剩一個 id 的話,
|
||||
# 誰都看不出剛剛不見的是什麼。
|
||||
print_record_line
|
||||
rm -f "$RECORD_FILE" 2>/dev/null || die 5 "刪不掉 $RECORD_FILE。請確認 $TASKS_DIR 可寫。"
|
||||
# rm 回 0 不保證檔案真的不在了:唯讀目錄底下的 rm 有可能什麼都沒做。所以回報之前再看一次。
|
||||
[ -f "$RECORD_FILE" ] && die 5 "$RECORD_FILE 還在,這一筆沒有刪掉。請確認 $TASKS_DIR 可寫。"
|
||||
printf 'removed=%s file=%s origin=%s spec_key=%s\n' \
|
||||
"$F_id" "$RECORD_FILE" "$F_origin" "${F_spec_key:--}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- 主流程 ---
|
||||
|
||||
RECORD_FILE=''
|
||||
@@ -730,6 +832,7 @@ case "$CMD" in
|
||||
fail) cmd_fail "$@" ;;
|
||||
pause) cmd_pause "$@" ;;
|
||||
resume) cmd_resume "$@" ;;
|
||||
remove) cmd_remove "$@" ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
exit $?
|
||||
|
||||
Reference in New Issue
Block a user