以簡訊為介面的產品
這個訴求很真實:人們不喜歡安裝應用程式或瀏覽網站,但每個人都會傳簡訊。問題是:你並不擁有這個頻道。 美國的電信業者擁有,他們有內容政策和審批流程,可以永久拒絕你。這個關卡決定了產品被允許成為什麼樣子,所以必須優先處理。
第零步 —— 關卡。在設計之前就做,而不是之後。
自 2025 年 2 月起,美國電信業者直接封鎖未註冊的 A2P 流量,因此品牌和行銷活動必須在發送第一則簡訊之前獲得批准。每次都要重新閱讀最新的錯誤文件;電信業者政策會變動。
| 代碼 | 觸發條件 | 可重新提交? |
|---|---|---|
| 30953 | 露骨/暗示性語言,或連結的網站包含「色情、交友、約會或成人娛樂內容」 | 否 |
| 30885 | 「高風險」— 欺騙性行銷,或註冊時虛報流量類型 | 否 |
| 30898 | 同一個 EIN 下註冊太多品牌,或註冊看起來重複 | 是,有正當理由 |
規則:在第一次回覆中明確說出產品的內容類別,並說明它是否在那個清單上。 如果這個類別(例如約會)恰好是拒絕規則中的關鍵字,那麼在第五分鐘就發現它,會改變架構、登陸頁面文案和備援計畫。SHAFT 是速記法:性、仇恨、酒精、槍械、菸草——再加上大麻,而約會也緊跟在旁。
規則:將附屬專案註冊為「獨資經營者」品牌,而非放在現有公司的 EIN 之下。 風險模式會跟著稅號(30898)跑,所以附屬專案被拒絕不能影響你主要業務的發送能力。「獨資經營者」的範圍是沒有稅號的個人;吞吐量很低,但對測試版來說已經足夠。
規則:絕不要為了讓行銷活動通過審查而建立一個誘餌登陸頁面。 一個隱瞞實際流量的頁面就是 30885 模式,它會把一個號碼的風險轉變為你所有已註冊號碼的風險。直接陳述成功機率,讓負責人決定。
用機械性的行為來描述服務,而且只有在描述為真時才這樣做(例如日記和小組問責,當這就是訊息內容時)。如果沒有真實的描述能通過審查關卡,答案就是改用其他頻道,而不是找一個更好的委婉說法。
審查會獎勵那些可以指向的事實:僅接收的選擇加入、對話式的流量、重複訊息中沒有連結、優先處理 STOP 指令、在入口處拒絕露骨內容。
第零點五步 —— 閱讀需求摘要時要找「數字」和「物件」,而不是意圖
將創辦人的需求摘要一字不差地保留為 README.md,並在 CLAUDE.md 中說明它凌駕於所有工程文件之上。
規則:在編寫狀態機之前,列出摘要中的每個持續時間以及它作用於的每個名詞。如果有兩個持續時間,那就是兩個功能。 一份需求摘要中,對朋友回覆送達作者 有一週的延遲,而對作者自己的更新 有兩週的鎖定,很容易被合併成對條目 的七天保留——而這個條目並非兩個延遲所針對的物件——後續的每一個錯誤都源於這個合併。
規則:命名每個延遲的物件——「對誰保持『擱置』」。 對朋友保持擱置和對作者保持擱置是兩種相反的產品。
規則:當頻道無法強制執行需求摘要中的字面設計時,直接說出來並給出替代方案——不要默默地選擇可強制執行的方案。(群組聊天的建立者同時也是其成員,所以沒有程式碼可以對他們隱藏群組的回覆。)這是一個前提決策,而前提決策屬於產品負責人。
一個連貫的誤讀不會讓人感覺像誤讀。 保留已被取代的設計段落,並加上刪除線,而不是將其刪除;這可以讓後續的會話知道需求摘要如何被誤讀。
第一步 —— 頻道能做些什麼和不能做些什麼
- 從程式碼進行群組簡訊是可行的。 Twilio Conversations 僅在 +1 長碼上支援群組 MMS——不支援免付費電話、不支援短碼——限制為包含你自己號碼在內最多 10 位參與者。不要憑記憶做出相反的斷言。
- 群組 MMS 的 SMS 參與者帶有 Address 而沒有 ProxyAddress,因此一個號碼可以服務所有聊天室——但參與者/代理唯一性規則(50425)對於沒有代理的參與者沒有記錄的行為。絕對不要設計許多共用一個號碼和一組參與者的並發聊天室。 每人一個持久聊天室具有穩定的參與者集合。
- 網頁無法讀取 iPhone 的通訊錄。 Contact Picker API 僅限 Chrome on Android。「同步你的聯絡人」從一開始就不可行;用一個使用者轉發的連結來取代它——接收者的傳入簡訊就是電信業者想要的同意記錄。
- Telegram 機器人無法建立群組或將任何人加入群組,且在隱私模式下它只聽取發送給它的指令。這兩點都有幫助:建立者將機器人加入他們已有的聊天室,而一般聊天內容不會被誤認為輸入。建立機器人需要在 @BotFather 中由真人操作(沒有 API);索取 token。
- WhatsApp Cloud API 沒有群組訊息功能。
- iMessage API(Sendblue、LoopMessage)可以實現藍色泡泡和真實群組聊天,但每個號碼每月大約花費 100 美元,加上設置費。
第二步 —— 能承受頻道遺失的架構
因為頻道可能被拒絕,傳輸層必須是一個檔案:
machine.js 純函數:(context, event) -> {replies, effects} 無 I/O,無時鐘
engine.js 上下文組裝 -> machine -> effects 完整路徑
effects.js createEffects({db, channel}) 執行決策
twilio.js / telegram.js 可互換
api/*.js 包覆 engine.js 的薄層 ~10 行程式碼
sim/ 記憶體資料庫 + 控制台頻道
- 決策核心將
now作為參數,且不接觸網路。 這類產品的每個錯誤都是時序錯誤(重複觸發的提醒、發送給空聊天室的訊息、從未送達的擱置項目),而且這些錯誤都無法在真實網路和七天計時器上進行測試。 - 配接器在效果層中是參數,而非匯入——這正是模擬器和備援頻道所需的相同重構。
- HTTP 處理程式中不能包含邏輯。 寫在
api/telegram.js中的頻道特定引導流程是無法測試的;將其提取出來。
第三步 —— 驅動它,而不僅僅是測試它
node sim/drive.js 指令碼:完整循環,跳過一週,檢查守衛條件
node sim/drive.js --repl 以任何號碼輸入;/as <n> /jump <days> /state
- 模擬器必須執行生產路徑,而不是一個並行的副本。
- 預期第一次驅動會發現幾個單元測試無法達到的真實缺陷:一個聊天室在成員加入之前就向自己打招呼;一個是裸露的星期幾而非一週後的日期;一個提醒沒有說明它指的是哪個項目;一個聊天室的成員全部離開了。
- 當測試失敗時,先檢查測試本身(一個日期正則表達式不允許
Sun, Aug 30中的逗號;一個doesNotMatch錯誤地觸發於另一個合法的訊息)。 - 絕不要根據資料庫儲存的時間戳來做決策或列印資訊。 資料庫從真實時鐘寫入
created_at,而不是來自引擎注入的now,因此在跳過時鐘的情況下它會說謊(並列印出過去的日期)。只根據機器寫入的欄位來做決策,並將相關日期掛在一個儲存的錨點上。
第四步 —— 適合鎖定螢幕的文案
一個檔案存放所有對外輸出的字串。
- 沒有表情符號、沒有驚嘆號、沒有行銷用語。
- 對於超過一兩天後的日期,絕不要使用裸露的星期幾。 例如
Sun Aug 30。 - 對每個關鍵字都容許標點符號和大小寫錯誤——
Wait.和Yes!都必須能匹配成功。 - 包含關鍵字的散文仍然是散文:「we should wait and see」不是 WAIT。匹配整個規範化後的訊息,絕不匹配子字串。
- STOP / HELP / START 在所有應用程式狀態和任何模型呼叫之前被解析,這樣退出功能就不會因為其他地方有錯誤或 API 故障而失效。
- 如果模型負責把關內容,失敗時應關閉保護:如果檢查無法執行,則不發送任何內容並告知使用者。
第五步 —— 如果模型改寫使用者內容,在信任它之前先執行檢查
一個從使用者條目中移除第三方姓名的守衛可能讀起來完美,但仍然會:
- 虛構一個事實——被要求將「Jane Doe」替換為「一個自然的代稱」,它寫出了「應用程式裡的那個女孩」,但原文完全沒提到應用程式;接收者會將虛構內容視為事實。
- 過度修正並刪除真實細節——「跑步俱樂部的 Jane」變成了「她」。
規則:指令必須說明模型「不得」虛構什麼以及「不得」刪除什麼。 可行的形式:將替換建立在來源的基礎上,替換符號而非其周圍的片語,並且在重複使用句子已陳述的細節時,優先使用中性的備案。
規則:將每個線上模型的缺陷固定為可選測試,在沒有 API 金鑰的情況下跳過,這樣提示詞的修改就不會在每次測試運行中都無聲地回歸這些缺陷,同時增加成本。每次守衛呼叫預留 2-5 秒。
成本(數量級;需確認實際情況)
一個 +1 長碼每月大約一到兩美元;簡訊每段(雙向)不到一美分;MMS 幾美分。獨資經營者註冊是一次性幾美元加上少量月度行銷活動費用。Telegram 是免費的。
詢問 vs. 決定
- **詢問:**是否要提交一份誠實但可能被永久拒絕的註冊,或者從 Telegram 開始(涉及金錢和不可逆的結果);任何會改變產品本質的事。
- **決定:**架構、文案風格、狀態機、測試覆蓋率、先實作哪個配接器、日期格式、守衛行為。
在報告任何東西被封鎖之前,列舉每一個候選憑證(磁碟上一個用盡的 API 金鑰並不代表沒有另一個可用的)。
另見: build-remote-mcp-server(另一種「原生介面」模式)。