通知治理
每則推播先標級別,勿擾時段把不急的延後,每天彙整一則摘要,並量測每類通知到底有沒有人理。
問題:所有通知共用同一條路
Section titled “問題:所有通知共用同一條路”DuDuClaw 的 AI 員工會主動找你:任務卡關、預算用完、高風險動作要你點頭、演化迴圈停滯、通道送不出訊息。這些訊息各自都合理,但在 v1.54 之前它們共用一個問題:沒有分級。凌晨三點的「演化事件」和凌晨三點的「任務卡住等你決定」用同一條路推出去,同樣會亮螢幕、同樣會出聲。
通知治理是擋在所有推播出口前面的那一層。它做四件事:把每則通知標上級別、在勿擾時段把不急的延後、每天彙整一則摘要、並且量測「這類通知到底有沒有人理」。
每則通知從產生到送達(或被延後)走這條路:
通知產生(NotifyLevel 必填,沒有預設值) | v 是 L3 嗎? --是--> 立即投遞(勿擾時段不擋 L3) | 否 | v 勿擾時段生效中? --否--> 立即投遞 | 是 | v 寫入 ~/.duduclaw/notify_queue.jsonl(附檔案鎖) | v 時段結束後由背景排程器投遞: - 同一收件目的地的 L1 合併成一則 - L2 決定卡單獨投遞 - 上限:最多 500 則、超過 36 小時丟棄(寫 warn 日誌)實作架構:單一推播入口
Section titled “實作架構:單一推播入口”決定卡有四種——目標停在 needs_human、待核可事項、安裝簽核、自動規則斷路器跳閘——各自有一個模組知道該怎麼措辭、該送到哪裡。措辭與目的地決定之後的每一步,現在都收斂成一個函式 notify_push::push(card, dest):
goal_notify ─┐approval_notify ┤install_notify ├──> notify_push::push(card, dest)autopilot_notify┘ | v 依 dest 解析該通道的 bot token | v decision_notify::deliver_outcome(等級與勿擾時段,見上節) | v channel_sender::send_with_markup / send_plain_textdest 決定用哪一套 token,而這正是以前四份實作各自寫錯的地方:卡片送給AI 員工時,用的是該員工自己的 [channels.<ch>] token,背後還有 reports_to 往上繼承;卡片送給儀表板上的真人時,用的是部署層的私訊 bot token,並逐一嘗試到其中一支真的能觸及這個人為止。
有兩件事刻意留在原本的四個模組、沒有搬進共用入口:卡片內容(只有各自的領域知道該怎麼寫),以及誰有權按下按鈕(decision_notify::authorize_press 加上各模組自己的送達紀錄)。推播是對外的副作用,絕不該有機會放寬誰能做決定。
四級階梯(escalation ladder)
Section titled “四級階梯(escalation ladder)”每個推播點都必須在程式碼裡標明級別(NotifyLevel),沒有預設值。新增一個推播出口時,你被強迫回答「這件事值不值得吵醒人」。
| 級別 | 判準 | 勿擾時段行為 | DuDuClaw 實例 |
|---|---|---|---|
| L0 | 沒有狀態變化 | 根本不進這一層 | 心跳、巡邏無事 |
| L1 週知 | 發生了,但不需要人 | 延後,醒來後合併 | 演化事件、SOUL 整併、技能缺口摘要、每日摘要、預算恢復 |
| L2 待確認 | 需要人知道並按一下 | 延後,醒來後單獨投遞 | 派工核准(任務還沒開始)、自動規則跳閘暫停 |
| L3 須處理 | 又急、又重要、又可行動、又是真的 | 照發,不擋 | 自主任務等你決定、高風險審批、安裝簽核、預算停工、通道故障 |
判準借自 Google SRE 的告警三分法(Page / Ticket / Report)。實務上的分界線是「等到早上有沒有代價」:派工核准等到早上只是晚八小時開工;任務卡在半路等你決定,是整條自主迴圈停擺。
L3 永遠不受勿擾時段影響,這是刻意的:一個可以被設定關掉的緊急通知不叫緊急通知。
# ~/.duduclaw/agents/<agent>/agent.toml[proactive]quiet_hours = "22:00-08:00" # 可選;未設定 = 不啟用timezone = "Asia/Taipei" # 可選;無法解析時用主機系統時區# ~/.duduclaw/config.toml — 全域退路[notify]quiet_hours = "22:00-08:00"員工自己的設定優先,沒有才吃全域值。跨午夜的區間(22:00-08:00)正常運作;區間是左閉右開,所以設 08:00 結束代表 08:00 整就會投遞。
解析失敗一律「不啟用」
Section titled “解析失敗一律「不啟用」”格式錯誤、空字串、start == end(例如 00:00-00:00)全部視為沒有勿擾時段,並在日誌留下 warn 記錄告訴你哪個值被忽略了。理由很直接:解析器的 bug 不該讓一個部署整晚收不到通知。設定寫錯的代價是「照常收到通知」,而非「靜悄悄什麼都收不到」。
舊欄位不會自動接管
Section titled “舊欄位不會自動接管”[proactive] 底下還有一組舊的 quiet_hours_start / quiet_hours_end(數字小時),它的預設值是 23–8,而且每個員工都有。治理層刻意不讀它,否則每個既有安裝都會在沒人要求的情況下,突然被靜音一整晚。那組欄位維持原本較窄的職責:排程 [proactive] 主動檢查的時間。
延後不等於丟掉
Section titled “延後不等於丟掉”被擋下的通知寫進 ~/.duduclaw/notify_queue.jsonl(附檔案鎖),時段結束後由背景排程器投遞:
- 同一個收件目的地的 L1 通知合併成一則(
🌙 勿擾時段收到 3 則通知:+ 編號清單),而非三則分開的訊息。 - 決定卡單獨投遞,因為每張卡有自己的按鈕。
- 佇列有雙重上限:最多 500 則、超過 36 小時的直接丟棄(一天半前的「任務完成了」已經算不上新聞)。丟棄一定會寫
warn日誌,不會無聲消失。
已知限制:在勿擾時段內被人從儀表板處理掉的決定,早上仍會投遞一張過期的卡片。按下去會被該決定的儲存層以「此決定已處理」擋掉(fail-closed),所以代價是一張多餘的卡片,不會重複執行。
儀表板要說出來
Section titled “儀表板要說出來”agent.get RPC 的 proactive 區塊多回傳兩個欄位:
quiet_hours— 生效中的區間字串("22:00-08:00"),沒有勿擾時段時為nullquiet_hours_note— 一句可直接渲染的繁中說明,明確寫出「哪些會延後、哪些照常送達」
會被靜默的條件必須攤在使用者看得到的地方,這是硬要求,算不上加分項。
[notify]daily_digest = false # 預設關閉,需明確開啟daily_digest_at = "09:00" # 本地時間每天一則,推到 [general] default_agent 的 [proactive] 目的地,內容彙整過去 24 小時:
- 完成任務數
- 待你決定的件數(審批 + 安裝申請 + 卡在
needs_human的任務) - 學習事件數(
gvu_*/playbook_*/soul_*/evolution_*活動流事件) - 花費
- 通道異常告警次數
- 行動率過低的通知類別(見下節)
全部歸零的一天,不發訊息,也不會發一則「今日無事」。一個你每天都可以不看的摘要,會訓練你連該看的那天也不看。
上限硬性一則:狀態檔記錄「最後送出的本地日期」,重啟不會補送第二則;但 09:00 時 gateway 沒開機、11:00 才起來的話,當天的摘要仍會送出一次。
員工摘要(「你不在的時候」)
Section titled “員工摘要(「你不在的時候」)”2026-10-08 新增(尚未發行),與上面全部署的 [notify] daily_digest 分開。
[digest]enabled = true # 預設開啟;設 false 關閉hour = 8 # 當地時(0–23)timezone = "Asia/Taipei" # IANA 時區,預設 UTCexclude_agents = [] # 不列入的員工每天在 timezone 的 hour 之後,持有 gateway 實例鎖(duduclaw_core::gateway_instance)的 gateway 不呼叫任何模型,組出每位員工的摘要(duduclaw-gateway/src/digest.rs):上次摘要以來(最多回溯 7 天)完成的任務與目標、動態、待核准、已結算的持續任務執行與花費。沒事可報的員工不列。摘要存成 <home>/digest/<date>.json,顯示在儀表板首頁(RPC digest.latest,只列觀看者可讀的員工與任務),並送到每位啟用中管理員已驗證的連結通道。<home>/digest/state.json 在送出前用檔案鎖認領當地日期,所以重啟或第二個 gateway 都不會重送(認領後、送出前當機則跳過當天)。預設開啟(2026-10-08 擁有者決定):沒有 [digest] 區段時於 UTC 08:00 執行。enabled = false 可關閉;config.toml 存在但無法解析,或 [digest] 任一值不對,也視為關閉。
首頁上每個完成項目都有 👍/👎/「需要修改」(可附備註)。RPC digest.feedback 需要對該員工的 Operator 並通過任務可見範圍檢查;只有 done 的任務可以回饋。回饋附加到 <home>/feedback.jsonl(演化反思的使用者回饋訊號原本就讀這個檔),type 為 positive/negative/correction,並帶 source = "deliverable"、item_kind、item_id、verdict、user_id、note。主動訊息的「被忽略」判斷會略過這些列。
檔案。 每位員工的段落也列出這段期間交付的檔案(artifacts.jsonl 帳本中對外交付的列,最新五個),每個檔案都能給同樣的回饋:digest.feedback { artifact: { agent_id, archived_name }, verdict, note? }。屬於某個任務的檔案照該任務授權(不要求 done);沒有任務的檔案需要對該員工的 Operator。回饋列的 item_kind 為 artifact,item_id 為 artifact:<員工>/<封存檔名>。不存在的檔案與被拒絕的回覆相同。
通道按鈕。 通道訊息為顯示的項目編號([1]、[2]…;每位員工最多三個完成項目與三個檔案),在 Telegram、Discord、Slack、LINE 上為前四項附上 👍/👎/✏️ 按鈕(Discord 每則訊息最多五列按鈕,LINE 最多十三個快速回覆)。按下時的授權與 digest.feedback 相同:按的人的通道身分必須對應到已驗證、啟用中的儀表板帳號(按下時重新讀取),且對該員工有 Operator;任務可見範圍必須同時允許這個帳號與這個通道;任務必須是 done。在 Telegram、Discord、Slack 上還會先通過該通道本身的存取設定。按鈕無法附備註,所以從通道按「需要修改」不帶備註,回覆會請對方到儀表板補充。通道文字中,可見範圍沒有列出該通道的任務只顯示編號、不顯示標題(channel_may_read_task)。
勿擾時段。 通道投遞是一則 L1 通知,套用全部署的 [notify] quiet_hours(摘要不屬於任何單一員工)。時段內會以 digest 種類排入 notify_queue.jsonl,時段結束後由已存的摘要重新組出並送出,不與其他通知合併;每次送出在行動率統計記為 digest.daily。未在真實通道驗證。
回覆較慢時的進度狀態
Section titled “回覆較慢時的進度狀態”2026-10-08 新增(尚未發行),預設開啟(2026-10-08 擁有者決定)。
[channel_reply]interim_status = true # 慢的那一輪送一行狀態;設 false 關閉interim_status_secs = 8 # 0 = 關閉(最多 600)interim_status_show_task = false # 是否寫出員工正在跑的任務名稱外部通道(Telegram、Discord、Slack、LINE、WhatsApp、飛書、Google Chat、Teams、企業微信、釘釘;不含 WebChat 與內部工作階段)的回覆在 interim_status_secs 秒後仍什麼都沒顯示時,回覆本身的進度回呼會收到一行不經模型組出的狀態:已等多久,以及員工若另有已認領的任務看板工作,那項工作已跑多久。任務名稱只有在 interim_status_show_task = true 且任務可見範圍允許該通道讀取時才會出現,因為通道使用者可能是外部客戶。每一輪最多一次;通道沒有提供進度回呼就不送。可原地編輯的通道會把它當成進度訊息的第一個狀態,之後被答覆取代;其他通道會另發一則訊息(LINE 與 WhatsApp 會用到推播額度)。多數 CLI 回覆超過 8 秒,這類通道幾乎每輪都會多一則訊息;在意額度的話請設 interim_status = false 或調高 interim_status_secs。config.toml 無法解析或型別不對時視為關閉(duduclaw-gateway/src/channel_reply/interim.rs)。
如果這一輪員工的模型已回報正在思考,同一行改寫成「💭 已收到,正在思考(已 N 秒)。」。思考內容一律不會送到通道,通道只知道有在思考。各 runtime 要到後續版本才開始回報思考,在那之前這一行仍是「⏳ 已收到,正在處理…」。所有通道的進度都走同一份允許清單(工具動作、任務看板、仍在處理、這行狀態;WhatsApp 和以前一樣只送任務看板);工具步驟邊界、實際模型名稱、思考這類只給 dashboard 的事件不會送到任何通道。
每則通知送出時記一筆,每次有人真的把決定按掉時記一筆,存在 ~/.duduclaw/notify_events.jsonl。
duduclaw dashboard → notify.stats RPC{ "days": 30, "broken_threshold": 0.5, "min_sample": 10, "types": [ { "type": "decision.install", "pushed": 12, "actionable": 12, "acted": 5, "action_rate": 0.42, "broken": true }, { "type": "decision.goal", "pushed": 12, "actionable": 12, "acted": 12, "action_rate": 1.0, "broken": false } ]}broken 直接搬 Google SRE 的判準:準確率低於 50% 的告警就是壞掉的告警。條件是至少 10 筆「可按的」推播,且行動率低於 50%。
幾個刻意的設計:
- 同一張卡按兩次只算一次行動(用決定 id 比對,非單純計數),所以行動率不會超過 100%。
- 被拒絕的按壓不算行動:按了但被權限或狀態擋掉,是失敗的互動,構不成「這則通知有用」的證據。
- 純週知類(沒有按鈕可按)會回報
actionable: 0且永遠不會被標記 broken。說「FYI 的行動率是 0%」是同義反覆,稱不上發現。
儀表板圖表已實作,位置見「儀表板介面位置」一節。
通道故障記錄的 schema
Section titled “通道故障記錄的 schema”channel_failures.jsonl 有兩項調整:
-
channel欄位普及:所有能從 session id 推出平台的寫入點(channel_reply_silent/channel_reply_fallback/runtime_fallback_substitution/trajectory_anomaly/foresight_alarm)都補上channel,讓儀表板統一日誌能回答「這次失敗發生在哪個平台」。推不出平台的(cron / bus / heartbeat 這類內部 session,以及只拿得到工作目錄的 PTY fallback)寫null或省略,不假造歸屬。 -
恢復事件:通道從告警狀態回到正常時,寫一筆
{"event":"channel_recovered","channel":"telegram","reason":"recovered","resolved":true,"resolves":"telegram_send_failed","timestamp":"…"}舊的失敗行不會被改寫(append-only 稽核檔),消費端靠「同一 channel 有沒有更晚的
channel_recovered」判斷這次故障是否仍然相關。
⚠️ 連帶的行為修正:通道故障告警的判定條件從「有 channel 欄位」改成「event 在送出失敗白名單內 且 有 channel 欄位」。少了這一步,第 1 點會讓每次 LLM 逾時、每次軌跡異常都被當成通道斷線來告警。白名單目前只有 telegram_send_failed;要加入新項目,該事件必須真的代表「這個通道送不出訊息」。
儀表板介面位置
Section titled “儀表板介面位置”這幾項設定/資訊都有 RPC,但入口分散在不同頁面,直接列出:
| 設定/資訊 | 位置 | 說明 |
|---|---|---|
勿擾時段(員工自己的 quiet_hours,完整脈絡) |
AI 員工列表 → 選一位員工 → 編輯(/agents/:id/edit)→「自動化」分頁 |
進階區塊有一個 HH:MM-HH:MM 輸入框,格式錯誤會就地標紅;下方即時顯示這位員工目前生效中的說明句(quiet_hours_note)。 |
| 勿擾時段(跨員工快速切換) | 系統設定 →「主動行為」分頁 | 同一顆 [proactive] quiet_hours,頁面頂端有員工下拉選單可切換。若這位員工的新格式欄位是空的、但偵測到舊版數字式 quiet_hours_start/quiet_hours_end 曾被改成非預設值(代表操作者早年透過這個頁面設定過勿擾時段),會顯示轉換提示並自動代填成新格式,儲存後才正式生效,避免「以為改了勿擾時段,其實治理層完全看不到」這種靜默陷阱(W2-9)。 |
每日摘要開關([notify] daily_digest / daily_digest_at) |
系統設定 →「系統」分頁 | 開關 + 時間欄位;全域一則,非逐員工設定。 |
通知成效(notify.stats 行動率) |
分析報表頁 →「通知成效」卡 | 依「行動率量測」一節的 SRE 50% 判準列出各通知類型的推播數/行動率,行動率過低的類別長條標紅並附一句提示文字(不只靠顏色,避免色弱或黑白列印看不出來);純週知類(沒有按鈕可按)永遠不會被標記壞掉。 |
同一顆 [proactive] quiet_hours 有兩個編輯入口:員工編輯表單的「自動化」分頁是完整脈絡(跟通知目的地、check-in 排程放在一起);系統設定的「主動行為」分頁是給操作者不進員工詳情就能快速切換、查看多位員工勿擾時段的捷徑。兩邊寫的是同一個 agent.toml 欄位,存檔後立即互相反映,不會各自為政。
- 設計依據:
commercial/design/reviews/ux-redesign-2026-08/02-ux-methodology.md主題 4(P4-1、P4-4、P4-5、P4-6)與03-analogous-products.mdC1/C7/C8/C12 - 程式碼:
crates/duduclaw-gateway/src/notify_governance.rs、notify_stats.rs、notify_digest.rs - 前端:
web/src/pages/agent-form/EditAgentPage.tsx(自動化分頁)、web/src/components/settings/sections/ProactiveTab.tsx、SystemTab.tsx、web/src/pages/ReportPage.tsx - 相關功能:34-goal-loop.md(needs_human 決定卡)、23-autopilot-engine.md(規則跳閘通知)