feat(seed): 依委派清單種入與重建助理的內建定期檢查項
待辦簿做好了但是空的,也沒有東西會去填它。內建的定期檢查項該排哪些、什麼時候排、多久一次,這些資料就在委派清單裡——每一支非不交的技能都有時間點與週期兩欄。這一輪把清單接成待辦簿的資料來源。 反查靠新增的一個欄位,它非有不可。清單移除一支之後要刪掉對應那筆,但待辦簿的識別碼是建立時間加標題的雜湊,跟技能名無關。拿標題當鍵等於把措辭變成介面,改一個字舊那筆就再也認不出來,於是每次重建都刪不掉舊的又加一筆新的,同一個檢查每輪做兩次。拿動作當鍵,提醒類那十一筆全是同一個字、一支都分不出來,而觸發類那幾筆會撞上使用者自己交辦、動作剛好是同一支技能的那一筆——撞上就是把使用者交辦的事當成內建項刪掉。所以另立一欄記那一筆對應哪一支技能。 因為要加欄位,寫入端只能改存放那一支——它是唯一寫得出待辦檔的入口,這一輪沒有自己寫檔。連帶補上移除那個操作:規格要求不留孤兒,而原本六個操作一個都刪不掉。它對使用者交辦的那幾筆一律擋下、除非人親自帶強制旗標,而種入這一支一次都不帶。擋在單一寫入者這裡最省,那條規定寫在呼叫端的話,呼叫端每多一個就要各自再實作一次。 交出方式與動作是兩套詞彙,對映是這一輪的重點。含觸發就取技能名,其餘一律取提醒。觸發的定義就是呼叫既有技能、內容照那支技能自己的流程走,所以填技能名等於照判定結果做。巡檢與提醒那兩種沒有獨立入口——沒有任何腳本或技能名代表得了某一支技能的唯讀切片——這時候填技能名,助理下一輪就會把整支技能一路跑完,那正是切片交要防的事。所以填提醒:照時程提醒、指出入口,不動手。實測十六筆裡五筆是技能名、十一筆是提醒。 條件式交的三支一律不種入,但逐支吵出來。條件本身是散文,清單裡沒有機器讀得懂的條件欄位,所以條件成立了沒有現在只有人答得出來。種進去的代價有現成例子:其中一支的條件明寫要等它自家路徑不再帶版本號,而那個條件現在不成立,種進去助理每輪都會叫它、每輪停在第一支自家腳本,沒有錯誤、沒有輸出、心跳照寫,看起來完全正常。一個會無聲卡死的項目比一個缺掉的項目難查得多。不種入的代價是看不出為什麼少了它,用逐支印一行保留原因補掉。人確認過某一支條件成立就一支一支帶旗標放行,不給全部放行的旗標,那等於用一個決定蓋掉三個不同的條件。 種入與重建是同一段程式,啟動時每次都跑。不記跑過沒有,也沒有第一次旗標,要不要動手完全由現況決定。分成兩段的話,兩段各自回答該有哪幾筆這同一個問題,等其中一段改了判準就會一邊加一邊刪同一筆,每輪反覆。啟動時跑實際套用、查現況時跑唯讀預覽,巡檢那一輪兩個都不跑——無人值守那一輪移除一筆會把那一筆的執行紀錄與失敗次數一起弄丟,而清單同步到一半就會刪錯,破壞性清理留給人。 清單讀不到的三種情況一筆都不移除。讀不到時,清單上沒有與這台機器沒裝那個外掛分不出來,照字面跑會把所有內建項一次刪光,而且結束碼看起來完全成功。 清單上還在、還可交,只是時間點或週期換了值的那種情況,規格三條規則一條都沒講到。處置是預設只報差異不改:存放那一支刻意沒有編輯操作,改值只能移除再重登,那會換識別碼、把執行紀錄與失敗次數歸零,一個已經連續失敗五次的項目會看起來像全新的。人要換就帶旗標。 跨外掛相依宣告到清單所在的那個外掛,版本下限取現行的發行版——那是清單與它的欄位說明都已經在上面的版本。寫更低的下限會讓一台裝著舊版、清單還不存在或欄位不同的機器通過相依檢查,然後在讀清單那一步才失敗。清單路徑照今天剛改的規則走,由叫用時餵進來的根目錄組成,那一支自己不解。 三份 manifest 的版號一併從 0.1.8 升到 0.1.9,並在相依欄加上清單所在的那個外掛。這一次沒有把版號分成獨立一筆:相依宣告與版號是同一個決定的兩半,宣告了新相依卻不升版,安裝端不會知道要重新檢查相依。
This commit is contained in:
+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