PTT 的鄉民大概都遇過:太久沒登入帳號就被砍。社群早期有 PTTAutoSign 這類工具,跑在 VPS 或 Raspberry Pi 上排程登入。可是為了一個每天只跑一次的小腳本去顧一台機器,實在不太划算。

ptt-autosign-worker 把這件事搬到 Cloudflare Workers 上:每天 02:30 UTC 由 Cron Trigger 觸發,自動登入 PTT、抓登入資訊、丟到 Telegram。不用 VPS,不用 Docker,crontab 也省了,Free plan 就跑得起來。

為什麼是 Cloudflare Workers

原版的 PTTAutoSign 是 Python 寫的,吃 PyPtt 這類函式庫,內部會起一個 terminal emulator 處理 PTT 的 ANSI escape code 和狀態切換。要搬到 Workers 有幾個阻礙:Workers 沒有 Node.js 完整 runtime,nodejs_compat 也包不到 terminal emulator;相依太重的話 cold start 會明顯變慢;而且每次執行有 wall-clock 上限,Free plan 30 秒就要結束。

所以這版乾脆不用 terminal emulator,直接打 PTT 的 WebSocket gateway (wss://ws.ptt.cc/bbs/),靠一個小型的 login state machine 推進流程。整個 Worker 零 runtime 依賴,部署檔很小,cold start 幾乎感覺不到。

它做了什麼

每天排程觸發後,Worker 會對 PTT_ACCOUNTS 中每個帳號執行:

  1. 連上 wss://ws.ptt.cc/bbs/
  2. 送出 username,\rpassword\r(後面那個逗號是 PTT 的 UTF-8 協商觸發字元)
  3. 走過 login state machine — 處理重複登入踢人、any-key 提示等
  4. 進入 T → Q → username 抓「登入次數」、「信箱狀況」,並從歡迎橫幅抓「上次登入 IP」
  5. G\rY\r 登出
  6. 把結果丟到 Telegram

成功的訊息長這樣:

✅ PTT alice 已成功簽到
📆 已登入 1234 天
📫 信箱中已無新信件
🌐 上次登入來源 172.69.194.125
#ptt #20260504

失敗時會帶上 reason,方便排查:

❌ PTT alice 簽到失敗
原因:wrong_password
#ptt #20260504

Big5 與 UTF-8 並行解碼

PTT 的編碼是個老問題:使用者可以切 Big5 或 UTF-8 模式,連登入過程中都可能切換。傳統做法是先猜編碼、解錯了再 fallback,但這在串流式的 WebSocket 上特別麻煩,狀態機可能因為解錯字就錯過某個關鍵 prompt。

這版的解法很土砲:直接開兩個 TextDecoder,Big5 跟 UTF-8 並行解碼,state machine 對兩條解碼結果各比對一次,誰先看到 prompt 就誰過。PTT 當下用什麼編碼都能正確推進。

Free Plan 的時間預算

Free plan 的 scheduled handler 有 30 秒 wall-clock 上限。每個帳號的時間預算抓起來大概是 10–17 秒,最壞情況 22 秒。所以 2 個帳號剛好塞進 Free plan,3 個以上比較建議升 Paid plan。

PTT 那邊改了 prompt 文字導致狀態機卡住的話,超過 15 秒會直接 timeout,不會把整個 handler 拖死。

路由

Worker 開了三個路由方便操作和除錯:

Route用途認證
GET /spike開 WS 連到 ws.ptt.cc,把歡迎橫幅原文 dump 出來
GET /run?secret=<RUN_TOKEN>手動觸發一次(等同 cron)RUN_TOKEN
scheduled()每天 02:30 UTCCloudflare Cron Trigger

/spike 在 PTT 改版時特別有用,直接看橫幅原文比較容易判斷 prompt 字串怎麼變。/run 則是部署後的 smoke test 入口:

curl "https://ptt-autosign.<sub>.workers.dev/run?secret=<RUN_TOKEN>" | jq

部署

不需要 KV,也不需要 D1,純粹靠 secrets:

npm install
npx wrangler login
npx wrangler deploy

npx wrangler secret put TELEGRAM_BOT_TOKEN
npx wrangler secret put TELEGRAM_CHAT_ID
npx wrangler secret put PTT_ACCOUNTS    # JSON: [{"id":"alice","password":"..."}]
npx wrangler secret put RUN_TOKEN       # 任意亂數字串

PTT_ACCOUNTS 是 JSON 陣列,一次部署就能幫一整批帳號簽到。

已知的軟肋

幾個 PTT 改版時可能會踩到的點,README 也直接列了出來:

getUser() 用的 regex 登入次數》(\d+)信箱[^》]*》 是根據經典版 user card 推測的,PTT 換 layout 就抓不到。不過抓不到只會讓 Telegram 訊息少幾行,登入本身不會失敗。

lastLoginIp 同樣是 best-effort。第一次登入沒有 IP(PTT 顯示「歡迎您第一次拜訪本站」),那行 🌐 會直接省掉。另外,這裡抓到的 IP 是 PTT 看到的入站 IP,對 Cloudflare Workers 來說會是 CF 的 egress IP(172.69.x.x 那類),不是你家的對外 IP。

Cron 精準度方面,Cloudflare 的 cron 會在指定時間的數秒內觸發,沒有保證準時。在意被偵測規律的話,可以在 scheduled() 裡自己加 jitter。

收尾

把每天跑一次的小工作搬到 Cloudflare Workers 上挺合理:cron trigger、secrets 管理都內建,不用自己顧伺服器,Free plan 足夠。比起再開一台 VPS 或佔用家裡的 Raspberry Pi,維運成本基本上是零。

對 PTTAutoSign 的使用者來說,這個 port 保留了原版的核心行為(登入 → 抓資訊 → 推 Telegram),但所有 runtime 依賴都拿掉了。整個專案只有三個 TypeScript 檔案:index.tsptt.tstelegram.ts。要客製化或加新功能都不難。

專案原始碼:GitHub