bugger/docs/TECH_INVESTIGATION.md

34 KiB
Raw Permalink Blame History

Feishu Connector — Technical Investigation Report

Date: 2026-06-30 Goal: Replace polling-based Bitable sync with event-driven (push) notifications from Feishu to Bugger.


TL;DR

⚠️ Conditionally feasible. The best approach is a WebSocket Long Connection to Feishu's event platform, which does NOT require a public-facing server. A feishu-connector service can maintain this connection, listen for drive.file.bitable_record_changed_v1 events, and notify the Bugger macOS app to refresh.

Critical caveat: Feishu only allows the Bitable owner or manager to subscribe to events (§7.1). For editor/viewer users, the connector cannot subscribe — Bugger falls back to polling for those Bitables. This is a progressive enhancement on top of polling, not a replacement.

Phase 0 (before any code): Verify the subscribe permission boundary with Bugger's target user roles. If all target users are editors only, pivot to improved polling (§8.2).


1. Current State (Bugger)

Bugger currently polls Feishu's Bitable REST API:

Bugger (macOS) ──[Timer: every N min]──▶ GET /bitable/v1/apps/{appToken}/tables/{tableId}/records
                                            │
                                            ▼
                                       Feishu Cloud

Pain points:

  • Latency up to the poll interval (default 5 min)
  • Wasted API calls when nothing changed
  • Timer-based, no real-time awareness

Existing Bugger Feishu integration:

  • OAuth 2.0 with user_access_token + refresh_token
  • tenant_access_token obtained via app_id + app_secret
  • FeishuAuthService already handles both token types
  • FeishuService already handles Bitable record fetching

2. Feishu Event Subscription Options

Feishu provides three mechanisms to receive Bitable change events:

Aspect Detail
URL wss://open.feishu.cn/open-apis/ws/v1/events
Public server required? No — client initiates outbound WebSocket
Auth One-time tenant_access_token at connection time
Protocol Custom Protobuf frame (pbbp2.proto) over binary WebSocket
SDKs available Go, Python, Java, Node.js (no official Swift SDK)
Connection limit 50 concurrent connections per app
Heartbeat Ping/Pong every ~120 seconds
Timeout 300s without any frame → reconnect
Reconnection Must implement exponential backoff manually
Deduplication Use event_id field; platform delivers at-least-once

How it works:

┌──────────────────┐                         ┌──────────────────┐
│  feishu-connector │                         │   Feishu Cloud    │
│   (macOS service) │                         │                  │
│                   │  1. GET tenant_access_token                │
│                   │──── HTTP POST ─────────▶│ /auth/v3/...     │
│                   │◀─── {token} ───────────│                  │
│                   │                         │                  │
│                   │  2. WebSocket connect    │                  │
│                   │──── wss://open.feishu.cn/open-apis/ws/v1/events ──▶│
│                   │                         │                  │
│                   │  3. Auth frame (PbFrame) │                  │
│                   │──── {type:"auth", token}──▶│                │
│                   │◀─── ACK ───────────────│                  │
│                   │                         │                  │
│                   │  4. Subscribe to Bitable │                  │
│                   │──── POST /drive/v1/files/{token}/subscribe ─▶│
│                   │                         │                  │
│                   │  5. Events stream in     │                  │
│                   │◀── PbFrame {bitable_record_changed} ───────│
│                   │◀── PbFrame {bitable_record_changed} ───────│
│                   │                         │                  │
│                   │  6. Ping/Pong keepalive   │                  │
│                   │◀──────▶─────────────────│                  │
└──────────────────┘                         └──────────────────┘

Option B: Webhook (HTTP Callback)

Aspect Detail
Public server required? Yes — must have a public HTTPS URL
Auth Manual signature verification + decryption on every request
Response HTTP 200 within 3 seconds
Retry 15s, 5min, 1hr, 6hr (max 4 retries)

Not suitable — Bugger is a personal macOS tool with no public server.

Option C: Bitable Built-in Automation (No-Code)

Aspect Detail
Setup Configure in Bitable UI: Automation → "When record changes" → "Send HTTP Request"
Public URL required? Yes — HTTP request target must be publicly reachable
Payload Customizable JSON with field value interpolation
Granularity Per-table, per-field conditions supported

Not suitable — still needs a public URL, and has limited payload control.


3. The Bitable Record Changed Event

Event type: drive.file.bitable_record_changed_v1

Event Payload (key fields)

{
  "schema": "2.0",
  "header": {
    "event_id": "f7984f25108f8137722bb63cee927e66",
    "event_type": "drive.file.bitable_record_changed_v1",
    "create_time": "1603977298000000",
    "token": "066zT6pS4QCbgj5Do145GfDbbagCHGgF",
    "app_id": "cli_xxxxxxxx",
    "tenant_key": "xxxxxxx"
  },
  "event": {
    "file_type": "bitable",
    "file_token": "bTkAbFdN...",
    "table_id": "tblXXXXXXXX",
    "revision": 42,
    "operator_id": { "union_id": "...", "open_id": "..." },
    "action_list": [
      {
        "record_id": "recXXXXXXXX",
        "action": "record_edited",
        "before_value": { "field_id": "...", "field_value": "..." },
        "after_value": { "field_id": "...", "field_value": "..." }
      }
    ],
    "subscriber_id_list": ["..."],
    "update_time": 1603977298
  }
}

Important Notes

  • Formula field changes do NOT trigger events
  • Must call subscribe API first: POST /open-apis/drive/v1/files/{file_token}/subscribe?file_type=bitable
  • Only document owner/manager can subscribe⚠️ SHOWSTOPPER (see §7.1)
  • App needs both app identity AND user identity permissions for bitable:app or drive:drive
  • Events include before_value and after_value — useful for detecting what changed

3.1 Client-Side Event Filtering (Payload Reduction)

The raw bitable_record_changed_v1 event includes full before_value / after_value for every changed field. For a large Bitable record, this can be several KB — enough to exceed DistributedNotificationCenter's ~2 KB payload limit.

Feishu does NOT support server-side field filtering on the event subscription — the entire changed record is always delivered. However, the connector can filter client-side before notifying Bugger:

Feishu ──[full event]──▶ Connector ──[filtered: only changed fields]──▶ Bugger

Filtering strategies (client-side, in BitableEventHandler):

Strategy What Bugger Receives Payload Reduction
Assignee-only Only action_list entries where the changed field is the assignee/person column ~90%+ for typical records
Field whitelist Only action_list entries matching a configured set of field names/IDs Configurable
Change summary {table_id, record_id, revision, changed_fields: ["Assignee"]} — no before/after values ~95%+, fits easily in DNC
Wake-up signal only {table_id, revision} — Bugger does a full re-fetch Minimal (~100 bytes), simplest

Recommendation: Start with the wake-up signal only approach. It's the simplest, safest for DNC payload limits, and Bugger already knows how to fetch records. The connector just needs to say "table X changed, go fetch." This avoids the entire payload-size concern and keeps the notification channel trivial.


4. WebSocket Long Connection Protocol Details

4.1 Protobuf Frame Format (pbbp2.proto)

message PbFrame {
  int32  method  = 1;  // 0 = CONTROL, 1 = DATA
  bytes  payload = 2;  // JSON string for DATA frames
  map<string, string> headers = 3;  // message_id, seq, sum, etc.
}

4.2 Connection Lifecycle

1. Obtain tenant_access_token
   POST /open-apis/auth/v3/tenant_access_token/internal
   Body: { "app_id": "...", "app_secret": "..." }

2. Open WebSocket
   wss://open.feishu.cn/open-apis/ws/v1/events

3. Send Authentication Frame
   PbFrame { method=1, payload='{"type":"authentication","data":{"tenant_access_token":"..."}}' }

4. Send Subscribe API (over HTTP, NOT websocket)
   POST /open-apis/drive/v1/files/{file_token}/subscribe?file_type=bitable

5. Receive Events
   Binary frames arrive. Parse PbFrame → extract JSON payload → process event.

6. Heartbeat
   Server sends ping_interval. Client must send CONTROL frames at that interval.

4.3 Fragment Reassembly

Large events may be split across multiple PbFrames. Headers provide:

  • message_id — groups fragments of the same logical message
  • sum — total fragment count
  • seq — 0-indexed fragment number

Client must buffer and reassemble by message_id.

4.4 Deduplication

Platform uses at-least-once delivery. Use header.event_id for idempotency.


5. Architecture Proposal: feishu-connector

5.1 High-Level Design

Key architectural principle: Polling is the primary path; push is a progressive enhancement. The connector provides real-time notifications only for Bitables where the user has owner/manager permissions (§7.1). For all others, the existing PollerService remains the sole refresh mechanism. Bugger operates in a per-Bitable mixed mode.

┌──────────────────────────────────────────────────────────┐
│                    User's Mac                             │
│                                                           │
│  ┌──────────────────────────────────────────────┐        │
│  │           feishu-connector (new)              │        │
│  │                                               │        │
│  │  ┌──────────────┐   ┌──────────────────┐     │        │
│  │  │ WSClient     │   │ EventDispatcher   │     │        │
│  │  │ (URLSession  │──▶│ - dedup           │     │        │
│  │  │  WebSocket)  │   │ - fragment merge  │     │        │
│  │  │              │   │ - filter: only    │     │        │
│  │  │              │   │   eligible tables │     │        │
│  │  └──────────────┘   └────────┬─────────┘     │        │
│  │                              │                │        │
│  │                     ┌────────▼─────────┐     │        │
│  │                     │ LocalNotifier     │     │        │
│  │                     │ (DNC wake-up      │     │        │
│  │                     │  signal: ~100 B)  │     │        │
│  │                     └────────┬─────────┘     │        │
│  └──────────────────────────────┼──────────────┘        │
│                                 │                        │
│  ┌──────────────────────────────┼──────────────┐        │
│  │           Bugger (existing)  │               │        │
│  │                              │               │        │
│  │  ┌────────────────┐  ┌──────▼──────┐       │        │
│  │  │ PollerService   │  │ Connector   │       │        │
│  │  │ (PRIMARY path   │  │ Listener    │       │        │
│  │  │  for ALL tables)│  │ (new)       │       │        │
│  │  │                 │  │             │       │        │
│  │  │ Interval:       │  │ On signal:  │       │        │
│  │  │  5 min (no WS)  │  │  fetchNow() │       │        │
│  │  │  15-30 min (WS  │  │             │       │        │
│  │  │  active)        │  │             │       │        │
│  │  └────────────────┘  └─────────────┘       │        │
│  │                                               │        │
│  │  Per-Bitable mode:                            │        │
│  │    ✅ owner/manager → WS push + polling       │        │
│  │    ❌ editor/viewer → polling only            │        │
│  └──────────────────────────────────────────────┘        │
│                          │                                │
└──────────────────────────┼────────────────────────────────┘
                           │
                           │ wss://open.feishu.cn
                           ▼
                   ┌───────────────┐
                   │  Feishu Cloud  │
                   └───────────────┘

5.2 Component Breakdown

Component Language Responsibility
feishu-connector Swift (macOS CLI/daemon) WebSocket connection, event processing, local notification
WSClient Swift Raw WebSocket + Protobuf frame encode/decode
EventDispatcher Swift Dedup, merge fragments, route events to handlers
BitableEventHandler Swift Parse bitable_record_changed_v1, filter to eligible tables only, emit wake-up signal
SubscriptionManager Swift Call subscribe API, detect permission errors, maintain eligible-table whitelist
LocalNotifier Swift Notify Bugger via DistributedNotificationCenter (wake-up signal: ~100 B)
Bugger Connector Listener Swift (in Bugger) Receive wake-up signals, trigger PollerService.fetchNow() for the signaled table
Bugger Eligibility Check Swift (in Bugger) On Bitable setup, test if subscribe succeeds; set per-table mode (push+poll vs. poll-only)

5.3 Notification Channel Options (connector → Bugger)

Method Pros Cons
DistributedNotificationCenter Native macOS, no socket, both Swift No guaranteed delivery, ~2 KB payload limit
XPC Service Native, secure, lifecycle management More complex setup, tightly coupled
localhost HTTP (e.g., :18924) Simple, debuggable, any language Port management, local firewall issues
Unix Domain Socket Fast, secure, no port conflicts Slightly more code
File watch / shared memory Simplest Polling defeats the purpose

Recommendation: DistributedNotificationCenter with wake-up-signal pattern.

The connector sends a minimal payload — just {table_id, revision} (~100 bytes) — and Bugger does a full re-fetch. This eliminates the payload-size concern entirely (§3.1). The signal is idempotent and loss-tolerant since Bugger's PollerService runs as a periodic fallback.

Fallback: If DNC proves unreliable in practice, switch to localhost HTTP on a fixed port. The wake-up-signal approach keeps the payload tiny regardless of transport.

5.4 Integration with Bugger's Existing Auth

Bugger already has:

  • FeishuAuthService — obtains tenant_access_token, handles OAuth
  • TokenManager — Keychain-backed token storage
  • FeishuService — Bitable API client

The connector can reuse the same credentials (app_id, app_secret from Info.plist) and obtain its own tenant_access_token. Alternatively, Bugger can share its token via Keychain (same access group).

5.5 Fallback Strategy

The connector handles event loss gracefully:

Event received ──▶ Notify Bugger ──▶ Bugger.fetchNow()
                                         │
If connector disconnects:                │
  → Bugger's PollerService continues     │
    as periodic fallback                 │
                                         │
When connector reconnects:               │
  → Full re-fetch to catch missed        │
    events                               │

Bugger's existing PollerService should remain as a fallback (e.g., every 1530 min) to catch any events missed during connector downtime.


6. Implementation Complexity Assessment

6.1 Swift Protobuf Implementation

Feishu's WebSocket uses a custom binary protocol (pbbp2.proto). In Swift:

Option Effort Risk
SwiftProtobuf (Apple) Medium Official, well-maintained. Need .proto → Swift codegen
Manual binary encode/decode High Error-prone, but avoids dependency
Use Codable + manual frame parsing Medium Protobuf wire format is straightforward for this simple schema

The PbFrame schema is simple enough (3 fields: int32, bytes, map) that a manual binary encoder/decoder (~200 lines) may be viable and avoids a Protobuf dependency.

6.2 WebSocket in Swift

URLSessionWebSocketTask (iOS 13+ / macOS 10.15+) provides native WebSocket support:

let session = URLSession(configuration: .default)
let wsTask = session.webSocketTask(with: URL(string: "wss://open.feishu.cn/open-apis/ws/v1/events")!)
wsTask.resume()

// Send binary
let frame = try PbFrameEncoder.encode(authFrame)
wsTask.send(.data(frame)) { error in ... }

// Receive
func receiveNext() {
    wsTask.receive { result in
        switch result {
        case .success(let message):
            switch message {
            case .data(let data): handleBinaryFrame(data)
            case .string(let text): handleTextFrame(text)
            @unknown default: break
            }
            receiveNext() // Loop
        case .failure(let error): handleDisconnect(error)
        }
    }
}

6.3 Line Count Estimates

Component Estimated LOC Complexity
PbFrame encoder/decoder ~150 Medium
WSClient (connect, auth, heartbeat, reconnect) ~300 Medium
EventDispatcher (dedup, merge, route) ~150 Low
BitableEventHandler (filter eligible tables, emit wake-up signal) ~120 Low
SubscriptionManager (subscribe API, permission check, whitelist) ~100 Low
LocalNotifier (DistributedNotificationCenter) ~50 Low
ConnectorDaemon (main loop, signal handling) ~100 Low
feishu-connector total ~970
Bugger: ConnectorListener ~80 Low
Bugger: EligibilityCheck (test subscribe, set per-table mode) ~80 Low
Bugger: Integration changes ~100 Low
Bugger changes total ~260

6.4 Dependencies

Dependency Need
SwiftProtobuf Optional (manual encoder possible)
Foundation (URLSession, DistributedNotificationCenter) Built-in
Third-party None required

7. Risks & Mitigations

Risk Severity Mitigation
Subscribe requires doc owner/manager 🔴 Critical Showstopper. The subscribing user must be the Bitable owner or have manager permissions. If Bugger's user is a collaborator/editor only, the WebSocket approach is non-viable for that Bitable. See §7.1 for deep-dive and alternatives.
No official Swift SDK Medium Protocol is documented; implement manually. PbFrame schema is simple.
Protobuf complexity Low Use Apple's SwiftProtobuf, or hand-roll ~150 lines for the simple schema.
WebSocket disconnects Medium Auto-reconnect with exponential backoff. Bugger's PollerService is fallback.
Event loss during disconnect Medium On reconnect, trigger full re-fetch. PollerService runs as periodic safety net.
Feishu API changes Low Protocol is stable. Event schema versioned (schema: "2.0").
Connection limit (50/app) Low Single-user tool — 1 connection.
Token expiry (2h) Low Bugger already handles token refresh. Connector re-auths on WS reconnect.
App must be "published" Medium Feishu requires app version publication before it can subscribe to events. Acceptable for internal tool, but adds setup friction.
macOS sandboxing Medium If the connector ships inside Bugger's app bundle, the sandbox may block outbound WebSocket connections. Requires com.apple.security.network.client entitlement. If distributed outside the App Store (direct download), sandboxing may not apply. See §7.2.

7.1 Deep-Dive: Subscribe Permission (Showstopper)

Feishu's event subscription API (POST /drive/v1/files/{file_token}/subscribe) enforces a hard permission check:

Only the document owner or users with manager-level permissions can subscribe to events on a Bitable.

This is a platform-level restriction — there is no workaround, no admin override, and no scope escalation that bypasses it.

Impact on Bugger users:

User's Bitable Role Can Subscribe? WebSocket Viable?
Owner Yes Full push notifications
Manager Yes Full push notifications
Editor No Must fall back to polling
Viewer / Commenter No Must fall back to polling

What Bugger can do about it:

  1. Detect at setup time. When the user configures a Bitable in Bugger, call the subscribe API. If it returns a permission error, log it clearly and fall back to polling for that Bitable. Don't silently fail.

  2. Per-Bitable mode. Bugger can support a mixed mode — WebSocket push for Bitables where the user is owner/manager, polling for others. The connector subscribes only to eligible Bitables.

  3. User-facing guidance. If subscription fails, Bugger should tell the user exactly what's needed: "To enable real-time sync, ask the Bitable owner to grant you manager permissions, or ask them to install Bugger and set up the connector."

  4. Polling remains the universal fallback. Even with the connector, Bugger's PollerService is the baseline that works for all permission levels. The connector is a progressive enhancement, not a replacement.

Verdict: This doesn't kill the project — it scopes it. The connector is a best-effort push layer on top of polling, not a replacement for it. The architecture must treat polling as the primary path and push as an optimization available to owner/manager users.

7.2 macOS Sandboxing Considerations

If feishu-connector is distributed as part of Bugger's app bundle (vs. a standalone CLI):

Scenario Sandbox Applied? WebSocket Blocked?
App Store distribution Mandatory Needs com.apple.security.network.client entitlement (outbound only — granted by default in most templates)
Direct download + notarized Optional No issue
Standalone CLI binary N/A No issue
LaunchAgent daemon N/A No issue

Required entitlements (if sandboxed):

<key>com.apple.security.network.client</key>
<true/>

Outbound WebSocket (client-initiated) is the least restricted network operation — this entitlement is included in Xcode's default sandbox template. It should not be a blocker.

If the connector also listens on localhost (for the localhost HTTP fallback), it additionally needs:

<key>com.apple.security.network.server</key>
<true/>

Recommendation: Distribute the connector as a LaunchAgent daemon (via .plist in ~/Library/LaunchAgents/), not embedded in the app bundle. This avoids sandboxing entirely and is the standard pattern for macOS background services. If it must ship inside the bundle, add the network client entitlement explicitly.


8. Alternatives Considered

8.1 Go/Python Sidecar

Run the Feishu WebSocket client in Go or Python (using official SDK), notifying Bugger via localhost HTTP.

Pros Cons
Official SDK support Two runtimes to manage
Less protocol risk Distribution complexity
Faster to prototype User must install Go/Python runtime

Verdict: Viable fallback if pure Swift proves too difficult.

8.2 Improve Polling Instead

Shorten poll interval + use conditional requests (ETags / If-Modified-Since).

Pros Cons
Zero new infrastructure Still polling
Immediate improvement Feishu Bitable API may not support conditional requests
No Feishu platform config needed Higher API call volume

Verdict: Quick win, but doesn't solve the fundamental problem. Feishu Bitable List Records API does not support ETags.

8.3 Bitable Automation + ngrok

Use Bitable's built-in Automation to send HTTP requests to an ngrok tunnel → local server.

Pros Cons
Zero code for Feishu integration ngrok dependency
Simple to configure ngrok URL changes on restart
Fragile for a long-running tool

Verdict: Good for prototyping, not for production.


9. Recommendation

Build feishu-connector as a Swift CLI/daemon with per-Bitable eligibility

Guiding principle: The connector is a best-effort push layer on top of polling — not a replacement. Polling remains the universal fallback for all Bitables. The connector accelerates refresh for Bitables where the user has owner/manager permissions.

Phase 0 — Permission Feasibility Check (1 hour) ⚠️ Do this first.

  1. Using Bugger's existing auth, call POST /drive/v1/files/{file_token}/subscribe for a test Bitable
  2. Verify the user role required (owner vs. manager vs. editor)
  3. If the test user (editor role) gets 403, confirm the permission boundary
  4. Go/no-go: If none of Bugger's target users are Bitable owners/managers, the WebSocket approach is non-viable — pivot to improved polling (§8.2) or Bitable Automation + ngrok (§8.3)

Phase 1 — Prototype (1-2 days)

  1. Implement PbFrame encoder/decoder in Swift
  2. Implement WSClient with auth + heartbeat + exponential backoff reconnect
  3. Implement SubscriptionManager — call subscribe API, detect permission errors, build eligible-table whitelist
  4. Implement BitableEventHandler — filter events to eligible tables, emit wake-up signal only
  5. Print received events to stdout for validation

Phase 2 — Integration (1 day) 6. Implement LocalNotifier → Bugger via DistributedNotificationCenter (wake-up signal: {table_id, revision}) 7. Add ConnectorListener to Bugger that triggers fetchNow() for the signaled table 8. Add per-Bitable eligibility check in Bugger — test subscribe on setup, set mixed mode 9. Keep PollerService as primary/fallback (5 min when no WS, 1530 min when WS active)

Phase 3 — Hardening (1-2 days) 10. Fragment reassembly 11. Deduplication via event_id 12. Graceful shutdown 13. LaunchAgent .plist for auto-start (avoids sandboxing — see §7.2) 14. Re-fetch on reconnect to catch missed events

Total estimated effort: 35 days (after Phase 0 passes)

Quick Alternative

If the full Swift implementation is too heavy upfront, prototype with Python (lark-oapi SDK, ~50 lines) + localhost HTTP notification → Bugger. Note: The Python SDK handles Protobuf frames internally; use it for validation, then port to Swift once the approach is confirmed viable. See Appendix A for caveats about the spike code.


10. References


Appendix A: Spike Code — Python Prototype (~50 lines)

⚠️ Caveat: This is a quick validation spike only. It uses text-mode WebSocket (sends/receives JSON strings) for rapid prototyping. The production Feishu WebSocket uses binary Protobuf frames (pbbp2.proto), not JSON text frames. The real Swift implementation must handle binary frame encoding/decoding, fragment reassembly, and CONTROL vs DATA frame routing. Do not use this prototype as the basis for production code.

For quick validation before full Swift implementation:

# DISCLAIMER: Spike only — uses text WebSocket for fast prototyping.
# Production must use binary Protobuf frames per §4.1.
# Do not ship this.

import asyncio
import json
import websockets
import requests
import sys

APP_ID = "cli_xxx"
APP_SECRET = "xxx"
FILE_TOKEN = "bTkAbFdN..."   # Bitable token

def get_tenant_token():
    r = requests.post(
        "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
        json={"app_id": APP_ID, "app_secret": APP_SECRET}
    )
    return r.json()["tenant_access_token"]

def subscribe_bitable(token, file_token):
    r = requests.post(
        f"https://open.feishu.cn/open-apis/drive/v1/files/{file_token}/subscribe",
        params={"file_type": "bitable"},
        headers={"Authorization": f"Bearer {token}"}
    )
    print(f"Subscribe: {r.status_code} {r.json()}")
    # Check for permission error — if 403, user is not owner/manager (§7.1)

async def main():
    token = get_tenant_token()
    subscribe_bitable(token, FILE_TOKEN)

    async with websockets.connect(
        "wss://open.feishu.cn/open-apis/ws/v1/events"
    ) as ws:
        # NOTE: Production uses binary PbFrame auth, not JSON text.
        auth = json.dumps({
            "type": "authentication",
            "data": {"tenant_access_token": token}
        })
        await ws.send(auth)
        print("Connected & authenticated")

        async for raw in ws:
            # NOTE: Production receives binary PbFrame, not JSON text.
            event = json.loads(raw)
            event_type = event.get("header", {}).get("event_type", "")
            if "bitable_record_changed" in event_type:
                print(f"🔔 Bitable changed: {json.dumps(event, indent=2)}")
                # TODO: notify Bugger via localhost HTTP (wake-up signal only)

if __name__ == "__main__":
    asyncio.run(main())

Appendix B: PbFrame Swift Encoder Sketch

// Minimal Protobuf wire-format encoder for PbFrame
// See: https://protobuf.dev/programming-guides/encoding/

struct PbFrame {
    enum Method: Int { case control = 0, data = 1 }
    let method: Method
    let payload: Data
    let headers: [String: String]
}

enum PbFrameEncoder {
    static func encode(_ frame: PbFrame) -> Data {
        var data = Data()
        // Field 1: method (varint, wire type 0)
        data.append(encodeVarint(field: 1, wireType: 0))
        data.append(encodeVarint(UInt64(frame.method.rawValue)))
        // Field 2: payload (bytes, wire type 2)
        data.append(encodeVarint(field: 2, wireType: 2))
        data.append(encodeVarint(UInt64(frame.payload.count)))
        data.append(frame.payload)
        // Field 3: headers (map entries as repeated messages)
        for (key, value) in frame.headers {
            let entry = encodeMapEntry(key: key, value: value)
            data.append(encodeVarint(field: 3, wireType: 2))
            data.append(encodeVarint(UInt64(entry.count)))
            data.append(entry)
        }
        return data
    }

    static func decode(_ data: Data) throws -> PbFrame {
        var method = Method.control, payload = Data(), headers = [String: String]()
        var pos = 0
        while pos < data.count {
            let (fieldNum, wireType, adv) = try decodeVarint(data, pos: pos)
            pos += adv
            switch (fieldNum, wireType) {
            case (1, 0): // method
                let (v, adv) = try decodeVarint(data, pos: pos); pos += adv
                method = Method(rawValue: Int(v)) ?? .control
            case (2, 2): // payload
                let (len, adv) = try decodeVarint(data, pos: pos); pos += adv
                payload = data.subdata(in: pos..<pos+Int(len)); pos += Int(len)
            case (3, 2): // headers entry
                let (len, adv) = try decodeVarint(data, pos: pos); pos += adv
                let (k, v) = try decodeMapEntry(data.subdata(in: pos..<pos+Int(len)))
                headers[k] = v; pos += Int(len)
            default: throw DecodingError.dataCorrupted(...)
            }
        }
        return PbFrame(method: method, payload: payload, headers: headers)
    }

    // ... encodeVarint, decodeVarint, encodeMapEntry, decodeMapEntry helpers
}

Conclusion: Building a Swift-native feishu-connector daemon using WebSocket long connection is feasible with ~970 lines of Swift code for the connector and ~260 lines of changes in Bugger, with no third-party dependencies. The approach provides real-time push notifications for Bitables where the user is owner/manager, while polling remains the universal fallback. Prerequisite: Verify the subscribe permission boundary (Phase 0) before committing to implementation.