2026年5月10日 8 min read

兩塊餅都崩了:PTT Flex 的踩雷紀實

開發日記PTTVariable FontWebSocket

那段時間我畫了兩塊餅

一塊是想做完整的瀏覽器版 PTT 客戶端 Nuxt 3 配 Vue 配一個自己刻的 <PttBoard> 元件 直連 wss://ws.ptt.cc 那種

另一塊是把 client 做成 npm package 上架 login 自動化 文章列表 推文 parsing 連 InteractionEngine 狀態機都從 PyPtt port 過來

兩個月後 兩塊都崩了

崩的方式還不一樣 崩的原因卻是同一個 Orz

先講沒崩的部分

不然這篇看起來只剩抱怨

PTT Flex 真正的主角是字型 一個從 Sarasa Gothic Mono TC 改造出來的 Variable Font 帶一個叫 wdth 的軸 從 70 一路拉到 100

意思是同一個字級下 我可以叫瀏覽器把字「橫向壓窄」 不縮字級也能在手機上塞下完整 80 欄的 BBS 版面

這塊 Phase 1 加 2 早就做完了 fontTools 編譯 brotli 壓縮 BIG5 subset 框線字元手調 WOFF2 不到 500KB 測試覆蓋 91%

從頭到尾沒出過一次包

崩的是其他地方

第一塊餅:完整版 PTT API

我想做一個正式的 npm package — @ptt-flex/client 完整的 PTT structured API 誰要做 PTT 第三方應用都能拉去用

完整是什麼意思 就是 login 自動化 看板列表 文章內容 推文 parsing 全包 連 PyPtt 那邊的 InteractionEngine 狀態機都認真想 port 過來

做下去才知道 PTT 的 TUI 不是隨便對 prompt 就好的東西 換頁時的 ANSI escape 看起來像有規則 細看又破例 推文夾在文章內容裡 切片的邊界一換行就漂走 狀態機要嘛寫得超複雜 要嘛漏掉一半 case

撐到 v1.1.0 我打 tag 把它整個收進 archive/structured-api 分支 主線砍到只剩三個方法 connect / send / onRawData

從「PTT API 套件」縮成「一個會 decode Big5-UAO 的 WebSocket connector」

第一塊餅 縮了一半多

第二塊餅:瀏覽器版 PTT 客戶端

想法很美 Nuxt 3 加 Vue 3 加一個自己刻的 <PttBoard> 元件 ResizeObserver 監聽容器寬度即時算 wdthfont-variation-settings 一拉 畫面上 80 欄字一邊縮窄一邊保持原本字級 打開瀏覽器就能用 PTT 而且字壓得超漂亮

這塊最關鍵的假設是 瀏覽器要能直連 wss://ws.ptt.cc

第一輪 Spike 我用 Node 配 ptt-client v0.9.0 登入 跑得過 熱門看板 128 個全拉到 文章列表 標題作者日期推文數都吐出來 連文章內容雖然有點怪但也讀得到大部分

我看完結論寫了一句「套件可用」就把這個 Spike 收掉

覺得這部分穩了

兩個月後 我才知道這句結論寫得多隨便

撞牆與實證

事情是做 web 才爆出來

我把 client 接上 Nuxt 跑起來 WebSocket 一直 1006 連 client 都還沒登入就斷

換瀏覽器 換網路 關防毒 隱身模式 通通一樣

最後寫了一份 5 行的 HTML 就一個 new WebSocket("wss://ws.ptt.cc/bbs") 加四個 lifecycle handler

A 組 從 localhost:8080 開 → ERRORCLOSE code=1006 B 組 直接到 https://term.ptt.cc 開 DevTools console 貼同樣那行 → OPEN → 收 1024 bytes 的 PTT splash 漂亮地飛進來

唯一變項是頁面 origin

PTT 在 WebSocket handshake 用 Origin header 白名單 只放行 https://term.ptt.cc 其他來源 TCP RST 直接打掉

而 Origin 在 W3C 規範裡是 forbidden header 瀏覽器 JS 沒辦法改

最賭爛的是 client 自己的 PttClient.ts 註解早就寫白

Use in Node.js / testing to inject a WebSocket implementation that can set the Origin header

只有 Node 能偽造 Origin 瀏覽器不行

這段註解在 repo 裡躺了兩個月 我看不懂

因為我的 Spike 是 Node 跑的

收尾

兩塊餅崩的方式不太一樣

client 是內部複雜度頂不住 自己縮回 thin connector web 是外部限制硬擋 瀏覽器能不能連 wss 這件事 從第一天就被 Origin 鎖死了 我只是兩個月後才知道

但崩的根本原因是同一個

餅可以畫大 餅底下最危險的那塊磚要先用最便宜的實驗敲過

我那兩個月的工 其實只需要 10 分鐘 打開瀏覽器 DevTools 貼一行 new WebSocket("wss://ws.ptt.cc/bbs") 就能省掉

這個磚我兩個月後才敲

下次別這樣 Orz


不過字型沒崩

主線最後 pivot 成一個純靜態 demo 模擬 PTT splash 畫面 直接秀 wdth 軸即時壓縮字寬的能力

拉滑桿可以玩 直接拖瀏覽器寬度看字怎麼變窄也行

https://harry18456.github.io/ptt-flex/

兩塊餅崩了 這塊磚是好的