countercall 是做什麼的?
Pak Yanto 請了半天假,騎了四十分鐘車到 Samsat 櫃檯,卻因為他的影印本是 Folio 而非 A4 規格而被拒絕。他損失了一天的收入,而且仍然不知道明天還缺什麼資料。
公務櫃檯公布的必備文件清單常不完整、過時,或在櫃檯現場有不一樣的說法。唯一可靠的來源是辦公室電話,但那條線路經常佔線、經過語音選單層層轉接,而且常常要打到第四通才有人接聽。所以多數人不會打電話。他們直接跑一趟、排隊,然後因為一份文件而被拒絕。
這個技能會代替您打電話,並回傳一份可讓您截圖帶出門的檢查清單。
它很符合 CALL-E 的設計理念:低頻率、個人化、高風險的電話工作,處理的是人類原本需要花整個早上重撥的號碼。
開始之前
此技能會實際撥打電話到真正的公務辦公室。 請先向使用者確認:
- 他們要詢問的是哪項程序,須使用辦公室自己的說法
- 哪間辦公室,以及辦公室官網公布的電話號碼(E.164 格式)
- 他們希望現在就打電話
絕不推測電話號碼。 不從目錄、不相近辦公室、不猜測國碼。只有當人類從辦公室自己的公開網頁讀取到號碼後,該號碼才能進入流程。打錯號碼意味著自動撥號系統會聯絡到陌生人,這是此技能最嚴重的錯誤。
安全邊界
此技能僅收集櫃檯要求資訊。
- 它不會送出申請、預約、支付費用,或讓使用者有任何承諾。它只詢問並回報。
- 它不會提供法律或移民建議。如果承辦人員建議替代方案,請逐字記錄為「承辦人員所說的話」,絕不能當成建議。
- 承辦人員的口頭回答是資訊性,不具法律約束力,每份呈現的結果都會如此標示。規定會改變,且個別櫃檯有裁量權。
- 通話開始時會清楚表明這是自動化助理,並說明來電原因。如果對方拒絕回答,它會感謝對方並結束通話。不會繼續追問、重複詢問,或當天再次撥打。
- 每天每間辦公室每項程序僅限一通電話。 這些是公務服務專線,接待人員面前還有民眾在排隊。冪等性鍵(Idempotency-Key)強制執行此限制,請勿移除。
- 若承辦人員不確定,結果會標示為「不確定」。絕不會用常見情況填補空缺,也絕不會將模糊陳述呈現為事實。使用者可能會穿越整座城市,依據此答案行動。
- 僅在辦公室公布的營業時間內撥打電話。晚上十點鈴聲空響不是資料點,而是騷擾。
在將此技能套用至新機關前,請先閱讀 references/safety.md。部分受話對象根本不適合自動撥號系統。
它與逐字稿有何不同
大多數通話技能回傳文字,讓讀者自行解讀。此技能回傳一個固定的、驗證過的物件,且拒絕回傳其他內容。
Goal 的 result_schema 設定了 additionalProperties: false,並將四個欄位限制為列舉值——payment_method、appointment_required、originals_or_copies 和 clerk_certainty——每個都包含一個「承辦人員不知道」的值。如果通話回傳的格式不符,結果會被隔離,不會呈現。部分解析的檢查清單比完全沒有還糟,因為使用者會根據它行動。
完整契約請見 references/result-contract.md。
工作流程
1. 收集請求
必填項目:程序名稱、辦公室名稱、辦公室的 E.164 格式電話號碼,以及該號碼的公開來源 URL。
在進行任何其他操作前,先驗證號碼。 E.164 格式為一個加號、非零的國碼數字,後面跟 7 到 14 個數字:^\+[1-9]\d{7,14}$。其他格式一律拒絕並詢問使用者。絕不猜測國碼。讓本地格式號碼進入撥號器,就是打錯電話的原因。
執行 scripts/preflight.mjs 可在不實際撥號的情況下檢查號碼和契約。
2. 檢查公布的契約是否已偏離
每次執行前,讀取最新的 Goal 介面,並與此技能撰寫時的契約進行比較。
import { diffContract, publishedRunSpec } from './scripts/_lib.mjs';
const goal = await client.goals.get(GOAL_ID);
const drift = diffContract(PINNED_CONTRACT, publishedRunSpec(goal));
if (drift.length) throw new ContractDrift(drift); // 拒絕撥號
使用 publishedRunSpec(goal) 輔助函式,不要直接使用 goal.published_run_spec。 線上路徑格式和文件都寫成 published_run_spec.result_schema;TypeScript SDK 會以 camelCase 形式轉換為 publishedRunSpec.resultSchema。若僅以文件中的名稱讀取 TS 客戶端,會回傳 undefined 而非拋出例外——檢查機制會判定「沒有公布規格」,並拒絕每次撥號,同時看起來仍在正常運作。輔助函式會同時讀取兩種寫法。這讓我們損失了一整天,已在 repo 根目錄的 FEEDBACK.md 中記錄。
CALL-E 文件建議在部署變數變更前進行此比較。在每次執行時都做,只花費一次 API 呼叫,就能將無聲的失敗轉變為明確的拒絕。用過時的 Schema 撥打真實人員的電話,會產生看似正常但實際錯誤的結果。
3. 撥打一通電話
使用 goals.run,並搭配以辦公室、程序和日期為範圍的 Idempotency-Key,這樣重試就不會重複撥打公務專線:
countercall:{office}:{procedure}:{yyyy-mm-dd}:v1
4. 輪詢直到取得結果或錯誤
goals.waitForResult 有一個完成規則:當 result 或 error 其中之一非 null 時停止。已完成的通話可能短暫同時回傳 null,因為 CALL-E 正在解析結構化結果,請不要將那個視窗視為失敗。
5. 呈現結果,或誠實地回報失敗
全部八種已公布的 GoalRunError 代碼都對應到不同的結果。以下列表是 scripts/render.mjs 實作的項目——如果 CALL-E 新增代碼,請保持同步。
| 結果 | 使用者看到的內容 |
|---|---|
| 有效結果 | 檢查清單,包含承辦人員的逐字陳述和來源 URL |
no_answer | 「線路無人接聽」。無檢查清單 |
declined | 「辦公室拒絕回答自動化來電」。無檢查清單 |
result_invalid | 通話完成但答案不符合契約。已隔離 |
result_unavailable | 通話完成但未產生結構化答案。無檢查清單 |
result_failed | 答案無法處理成檢查清單。無檢查清單 |
timed_out | 截止時間前「無可用答案」。無檢查清單 |
call_failed | 通話無法完成。無檢查清單 |
canceled | 在產生答案前被取消。無檢查清單 |
沒有任何分支會呈現部分檢查清單。 每個結果的實際範例請見 references/examples.md。
執行方法
scripts/call.mjs 預設為乾燥執行。它會印出將要發送的確切請求,但不會撥打任何電話。需要加上 --live 才會實際撥號:
# 乾燥執行——印出將發送的請求,不撥號,不需憑證
node scripts/call.mjs --office <id> --procedure "<procedure>"
# 實際撥打電話
node scripts/call.mjs --office <id> --procedure "<procedure>" --live
預設會撥號的技能,就是會意外撥號的技能。
data/offices.json 出廠時電話號碼已遮罩(+62XXXXXXXXXX),因此預設的條目無法撥號,對它下達的任何命令都會以退出碼 3 並顯示 REFUSING TO DIAL。這是刻意的:只有當人類從辦公室自己的公開網頁讀取號碼,並記錄了 source_url 和 source_checked 後,號碼才能加入檔案。請在執行任何操作前加入您自己的條目。
若要立即查看乾燥執行的輸出,測試 fixture 中包含了一個有效的虛擬號碼:
node scripts/call.mjs --offices ../../test/fixtures/offices.test.json \
--office fixture-sourced --procedure "perpanjangan paspor"
何時不該使用此技能
- 緊急狀況。 這是需要數分鐘延遲的文件查詢,不是緊急聯絡方式。
- 擁有已公布、可靠且最新的文件要求的辦公室。 如果網站資訊正確,請直接查閱網站。只有在已知公布的答案不完整時,才值得打電話。
- 任何曾要求不要被自動化系統撥打的機構,或當地法律限制自動撥號的情況。加入辦公室前請先確認。
- 大量查詢。 每天每間辦公室每項程序一通電話是設計上限。如果您需要批量查詢,您需要的是資料共享協議,而不是電話。
- 任何「搞錯也無大礙」的情況。 此技能的價值來自於浪費一趟行程的代價。如果使用者可以乾脆直接去現場確認,那就讓他們去。
誠實的限制
- 涵蓋範圍僅限於種子檔案中的辦公室,每個辦公室都有公布來源 URL。
- 公務專線的接通率因辦公室和時段而異,且很大比例的電話將無人接聽。這種不可靠性正是此技能存在的原因,我們會如實回報,而非隱藏。
- 結果是某一天某位承辦人員的答覆。它是證據,不是保證。