# 策展 · X (Twitter) 🔥

> 📖 本站完整內容索引（documentation index）：[llms.txt](/llms.txt)

> 作者：Lindy (@getlindy) · 平台：X (Twitter) · 日期：2026-04-11

> 原始來源：https://x.com/getlindy/status/2042791001623028033

## 中文摘要

# 我們是如何將 iMessage 整合進 Lindy 的

在拉斯維加斯某個資料中心的機架上，曾有一台 Mac Mini 承載了 Lindy.ai 整個 iMessage 的基礎設施。它配對了一支 iPhone，擁有一個 Apple 帳號，並執行著一個 Swift 常駐程式（daemon），該程式會監控 SQLite 資料庫，並以 Apple 絕對不樂見的方式透過 Private Frameworks 注入訊息。我們開發了它，重寫了它，最後刪除了所有東西，並用一個 API 呼叫取而代之。一個月內經歷了三次徹底的重寫。這就是這三次重寫皆為正確決策的故事。

## 為什麼 iMessage 與其他管道截然不同

若要以程式方式發送簡訊，你會呼叫 Twilio 的 REST API。若要發送 WhatsApp 訊息，你會呼叫 Meta 的 Business API。Telegram 也是一樣。這些平台都希望開發者在它們之上進行開發。它們會發布文件、提供 API 金鑰，並按訊息量向你收費。

Apple 則完全不做這些事。沒有 iMessage Business API，也沒有針對 Messages 的開發者計畫。如果你想以程式方式發送 iMessage，你需要：

- 一台實體 Mac（或資料中心裡的 Mac）

- 一支與該 Mac 配對的實體 iPhone

- 一個登入 Mac 上 Messages.app 的 Apple ID

- 一個監控 Messages 資料庫以接收訊息，並透過未公開的框架注入發送訊息的常駐程式

這就是起跑線。每一家開發 iMessage 整合的公司都從同樣荒謬的處境開始。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1775915052307-diaHFUWTIakAAfwnVjpg.jpg)

## 第一座橋樑：Swift 常駐程式

2025 年 12 月下旬，團隊為 Lindy 的 iMessage 整合編寫了一份 RFC。架構如下：一個在資料中心 Mac Mini 上執行的 Swift 常駐程式，監控 Messages.app 的 SQLite 預寫式記錄檔（chat.db-wal）以獲取傳入訊息。至於發送，它使用 Apple 的 Private Frameworks 直接將訊息注入 Messages.app 的內部，確保藍色氣泡的顯示。AppleScript 則是備用方案。

這座橋樑成功運作了。它能發送和接收 iMessage、處理附件，並透過 webhooks 將所有內容轉發回 Lindy 的 API。程式碼很乾淨，文件也很詳盡。

但團隊中只有一位工程師懂 Swift。iMessage 橋樑運行在資料中心的實體硬體上。當它在凌晨兩點崩潰時，必須有人 SSH 進 Mac Mini 並除錯一個監控 SQLite WAL 檔案的 Swift 程序。如果只有一個人能做到這一點，這不僅是基礎設施的問題，更是團隊擴展性的問題。

健康檢查端點說明了一切：

```typescript
const iMessageBridgeHealthSchema = z.object({
    bridgeId: z.string(),
    status: z.enum(['healthy', 'unhealthy']),
    messagesAppRunning: z.boolean(),
    lastMessageSentAt: z.number().optional(),
    pendingCommands: z.number(),
    uptime: z.number(), // seconds
})
```

`messagesAppRunning: z.boolean()`。我們實際上是在檢查資料中心裡的 Mac 上，Messages.app 是否仍在執行。

團隊開始研究競爭對手在做什麼。OpenClaw 和其他人正在使用一個名為 BlueBubbles 的開源函式庫，這是一個用於控制 iMessage 的 JavaScript 引擎。整個團隊都能閱讀並除錯 JavaScript。

## 押注 BlueBubbles

遷移到 BlueBubbles 後，我們用執行在相同硬體上的 JavaScript 引擎取代了自訂的 Swift 常駐程式。同樣的 Mac Mini，同樣的 iPhone，同樣的 Apple ID。但現在，橋樑程式碼使用的是全團隊都能維護的語言。

這次遷移快速解鎖了多項功能。幾週內，團隊就實現了表情符號反應（reactions）和 tapbacks，並透過自訂的 BlueBubbles 建置支援了 Markdown 格式（粗體、斜體、刪除線）。編輯和取消發送功能也隨後跟進。

但真正的產品工作是讓 iMessage 感覺像是一個完整的訊息平台，而不僅僅是一個純文字管道。我們將 iMessage 視為一等公民管道，這意味著要支援使用者對原生對話所期望的一切：

語音備忘錄可以雙向運作。如果你發送語音備忘錄給你的 Lindy Agent，它會被轉錄並以文字形式傳送給 Agent。如果你要求 Agent 發送語音備忘錄給你（例如，在你開車時總結你的早晨行程），它會透過 ElevenLabs 生成音訊，並以 iMessage 語音備忘錄的形式發送回來。

反應功能也是雙向的。你可以對 Agent 的訊息做出反應（讚、愛心等），這會觸發 Agent 的動作，對於快速確認非常有用。Agent 也可以對你的訊息做出反應，提供輕量級的回饋，而無需發送完整的回覆。

圖片處理則更有趣。將照片發送給 Agent，多模態 LLM 會直接處理它。Agent 也可以回傳圖片：如果它從電子郵件附件中抓取了圖表，或者生成了你要求的內容，它會以原生 iMessage 圖片的形式傳送過來。每張圖片都會儲存在我們的基礎設施中，以便 Agent 日後參考。你可以在週六發送週末計畫的照片，並在週二詢問相關內容。Agent 依然可以存取它。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1775915052280-diaHFUXNZa8AQXYthjpg.jpg)

然而，硬體限制依然存在。每個 iMessage 號碼都需要自己的 Mac Mini、自己的 iPhone 和自己的 Apple 帳號。團隊考慮了競爭對手的做法：透過輪詢（round-robin）分配多個號碼，並共享聯絡人卡片，這樣使用者就不會注意到訊息來自不同的號碼。但計算結果行不通。一個號碼代表一台 Mac Mini、一支 iPhone、一個 Apple 帳號。擴展到十個號碼意味著所有東西都要乘以十。我們最終僅以單一號碼上線。

RFC 中的容量估算為每個 Apple ID 每小時 100 到 500 則訊息。團隊預計每天會有幾百則訊息。

## 被封鎖

上線當天，iMessage 的流量遠超我們所有的估計。訊息量遠比我們計畫的多，速度也比我們預期的快。

該功能運作得完全符合預期。使用者發現了 iMessage，嘗試了它，並持續使用。一個可以像同事一樣傳簡訊的 AI 助理，正是人們會頻繁使用的工具。

Apple 的垃圾訊息偵測是一個黑盒子。iMessage 沒有公開的速率限制，沒有關於觸發封鎖原因的文件，也沒有申訴流程。新帳號、高流量、接收者多樣性低以及發送與接收比例失衡，都會導致被封鎖。在 Apple 看來，一個成功的 AI 助理上線所表現出的行為模式，與垃圾訊息操作完全相同。

該帳號被永久封鎖。半天的 iMessage 功能，然後就沒了。

橋樑程式碼很穩固。BlueBubbles 處理了負載。這項功能顯然是使用者想要的。Apple 未公開的垃圾訊息閾值抓住了我們，再多的橋樑工程也無法改變這一點。

顯而易見的下一步（購買新 iPhone、設定新 Apple ID、配置新 Mac Mini）需要數天時間，花費更多金錢，卻無法解決任何問題。下一個帳號也會被封鎖，只是時間早晚而已。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1775915052271-iaHFUXR2ma8AArjvUjpg.jpg)

## 重建：一個 API 呼叫

當團隊在思考下一步時，我們的一位工程師聯繫了一家名為 Linq 的公司。Linq 是一項託管式 iMessage 橋樑服務。他們處理困難的部分（Mac Minis、iPhones、Apple IDs、號碼輪替、反垃圾訊息策略），並公開一個 REST API。你呼叫他們的 API，獲取電話號碼，發送訊息，接收 webhooks。除了 iMessage 之外，開發體驗與 Twilio 或 WhatsApp 相同。

重建過程花了四個小時，並使用 Claude Code 加速重寫。團隊已經兩次開發過 iMessage 整合。他們了解訊息路由、webhook 處理和附件處理。改變的是，他們不再需要擁有硬體層。

新的訊息發送程式碼：

```typescript
try {
    return await trySendViaLinq(actor, identity, phoneNumber, message, attachments)
} catch (error) {
    logger.info('Linq send failed, falling back to SMS', { error })
}
return await sendViaSMS(actor, identity, phoneNumber, message)
```

嘗試 Linq，失敗則退回到 SMS。就這樣。

幾天內，團隊完成了完整的遷移：已讀回條、附件、語音備忘錄、回覆串、聯絡人卡片生成，以及一種新的引導流程，讓使用者先傳簡訊給系統（這樣 Apple 看到的是雙向對話，而不是單向的轟炸）。

舊的橋樑程式碼在一次清理 PR 中被刪除：76 個檔案變更，數千行程式碼被移除。自動化 PR 審查員給了它 9/10 的評分，並補充道：「這主要是刪除，這是最好的程式碼類型。」

第二次清理完全移除了 `apps/imessage-bridge` 目錄。審查員評論：「這是一個刪除操作。你很難搞砸它，但也沒什麼特別的技巧可言。非常完美。」

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1775915052277-iaHFUXaypakAALmjEjpg.jpg)

## 讓它感覺像是在與人傳簡訊

隨著基礎設施在 Linq 上穩定下來，問題轉移了。iMessage 管道運作正常。訊息可靠地進出。但「運作正常」和「感覺對勁」是兩回事。一些粗糙的邊緣讓體驗感覺像是與系統對話，而不是與同事傳簡訊。

輸入中的氣泡（Typing bubble）

你傳簡訊給你的 Lindy Agent，要求它檢查你本週的行事曆。Agent 需要抓取你所有的行程、篩選它們，或許還要交叉比對一些事情。這需要一兩分鐘。從你的角度來看，你只看到已讀回條，然後是沉默。三十秒後，你會懷疑它是否壞了。一分鐘後，你會傳簡訊問「哈囉？」和「你還在嗎？」

Agent 運作正常。它正處於執行中，正在抓取行程並篩選結果。

解決方法是輸入指示器，也就是當有人正在撰寫訊息時你看到的跳動的三個點。當 Agent 開始處理時，我們顯示輸入氣泡。它會保持啟用狀態，直到發送第一則回覆訊息為止。如果輸入指示器消失但沒有訊息到達，那才是真的出了問題。如果它還在跳動，請稍候。

顯而易見的替代方案是發送狀態訊息：「處理中！」或「正在檢查你的行事曆...」。我們沒有這麼做，原因與封鎖有關。Apple 的垃圾訊息偵測會查看訊息比例。粗略的規則大約是每收到一則訊息，發送十則出站訊息，之後你就會看起來像個垃圾訊息發送者。每一則「處理中...」的訊息都會消耗掉這個額度。如果 Agent 在實際答案之前發送了三則狀態更新，那對使用者的一次查詢來說就是四則訊息。輸入指示器不消耗任何訊息額度。這是一個免費的訊號。使用者獲得了回饋，而我們的發送/接收比例保持乾淨。

防抖動（Debounce）問題

另一個粗糙的邊緣是：連珠炮式的訊息。人們傳簡訊的方式就像他們說話的方式。你發送「嘿」並按下 Enter。然後「我今天行程如何？」並按下 Enter。然後「舊金山天氣如何？」並按下 Enter。

如果沒有防抖動，Agent 會收到三則獨立的訊息，並啟動三次獨立的處理程序。你會收到三則獨立的回覆，一則回答「嘿」，一則關於你的行事曆，一則關於天氣。Agent 也會感到困惑，因為當它在回答「嘿」時，又有兩則它還不知道的訊息進來了。

解決方法是三秒的防抖動視窗。當訊息到達時，我們等待三秒。如果在這段視窗內有另一則訊息進來，我們重置計時器。一旦三秒過去且沒有新訊息，我們將所有內容打包，作為一個請求發送給 Agent。「嘿，我今天行程如何，還有舊金山天氣如何？」一則訊息，一個回應。

為什麼是三秒？時間太長，單一訊息的使用者會坐在那裡看著什麼都沒發生，以為它壞了。時間太短，你會錯過兩則訊息連發中的第二則。三秒是折衷方案。

我們在 Temporal 上建置了這個功能。每一則傳入的訊息都會發送一個訊號給 Temporal 工作流，該工作流會保持狀態並等待沉默，然後再進行分派。選擇 Temporal 而不是 Redis 之類的東西，是因為我們可以重試失敗的分派，在除錯時檢查工作流狀態，而且整個系統預設是持久化的。

這實際上在最初的橋樑上就運作過。我們在 Linq 遷移期間丟失了它，現在正在重建。這就是那種當它存在時你不會注意到，但當它消失時你絕對會注意到的功能之一。

## 尚未解決的問題

三種架構，每一種在當時都是正確的。當沒有 iMessage API 時，Swift 常駐程式是正確的第一步。當團隊需要每個人都能在凌晨兩點進行除錯時，BlueBubbles 是正確的第二步。在封鎖事件證明我們不應該經營自己的電話基礎設施後，Linq 是正確的第三步。

Linq 現在運行著八個電話號碼，在使用者之間進行負載平衡。反垃圾訊息策略包括每個號碼的每日訊息上限、使用者先傳簡訊的引導流程，以及動態聯絡人卡片生成，以便使用者可以儲存該號碼。當初導致帳號被封鎖的發送與接收比例，現在成了我們主動監控的指標。

但我們是在一個可以隨時、以任何理由、無需文件且無法申訴的情況下撤銷存取權的平台上進行開發。Linq 處理了操作複雜性，但根本風險沒有改變。Apple 明天可能會更改其服務條款，每一家開發 iMessage 整合的公司，包括 Linq 本身，都必須適應或關閉。

拉斯維加斯的 Mac Mini 已退役。訂閱已取消。我們在這個版本中使用了八個電話號碼、一個 API 和零台 Mac Mini，而 Apple 至今仍未發布任何一頁關於 iMessage 整合的文件。

## 標籤

iOS, macOS, 其他, Lindy.ai, Apple
