sms — Huawei 路由器簡訊讀取器¶
Terminology rule (zh-TW pages)
技術名詞首次出現以「中文 (English original)」格式呈現,例:依賴注入
(dependency injection)。不自創翻譯——若無公認譯名直接保留英文
(如 embedding、tokenizer)。代碼、API 名、CLI flag、套件名、檔名一律不翻。
CLI 與 TV channel,用來讀取 Huawei 4G/5G HiLink 路由器 (router) SIM 卡上的簡訊
(SMS) — 主要是為了不必開網頁 UI 也能拿到驗證碼。
sms watch 則加上推播 (push):輪詢 (polling) 路由器,把每則新簡訊轉送到任一
apprise 目標(Telegram、ntfy、Discord、桌面通知…)。
CLI 透過 huawei-lte-api
打路由器的 XML API(/api/sms/sms-list、/api/user/login 等),
它會處理 CSRF token、SCRAM-SHA-256 登入、以及 session cookie。
這比去刮 JavaScript SPA 可靠太多。
安裝¶
由 chezmoi 自動部署:
~/.dotfiles/bin/sms— CLI(uv-script;首次執行時會把依賴解到 uv 的 cache)~/.config/television/cable/sms.toml—tv smschannel~/.config/television/cable/sms-archive.toml—tv sms-archivechannel~/.config/sms/config.toml.example— 起始設定範本
需要 uv(已透過 bootstrap script 安裝)。
首次設定¶
# 方式一:填好 config 檔
cp ~/.config/sms/config.toml.example ~/.config/sms/config.toml
chmod 600 ~/.config/sms/config.toml
$EDITOR ~/.config/sms/config.toml # 設定密碼
# 方式二:讓 sms 在第一次呼叫時提示並幫你存
sms login-test
環境變數覆寫(優先順序高於設定檔):
| Variable | 用途 |
|---|---|
SMS_ROUTER_HOST |
路由器 IP / hostname(預設 192.168.168.1) |
SMS_ROUTER_USER |
登入使用者(預設 admin) |
SMS_ROUTER_PASS |
登入密碼 |
SMS_CACHE_TTL |
收件匣快取 TTL,秒(預設 30,0 停用) |
SMS_NOTIFY_URLS |
sms watch 用的 apprise URL,以空白分隔(覆寫 [watch] 設定) |
命令¶
sms # 最近一筆驗證碼 → stdout + 剪貼簿
sms code [--no-copy] # 同上,明確指定
sms latest [-n 5] # 最近 N 則訊息(漂亮表格)
sms list [--unread] [--json]
sms show INDEX [--json]
sms delete INDEX
sms refresh # 強制清快取並重新抓取
sms login-test # 驗證帳密 / 連線
sms count [--json] # 收件匣總數 / 上限 / 未讀(B525s 不需登入)
sms prune --keep N [--include-unread] [--yes] # 刪除最舊、超出 N 則的訊息(預設只刪已讀)
sms watch [--interval 10] [--once] [--json] [--no-notify] [--mark-read]
sms notify-test [--latest] [--dry-run] # 透過與 watch 相同的目標送一則測試推播
sms archive # 把收件匣所有訊息複製到本機 SQLite 封存
sms export [--format sbr|mbox|jsonl] [-o FILE] [--refresh]
sms search [WORDS…] [--phone P] [--since 2026-10] [--codes] [--limit 20] [--json] [--key K]
sms service install|uninstall|status # `sms watch` 的 systemd --user unit(Linux)
Exit code:0 成功,1 路由器錯誤,2 設定/參數錯誤,3 找不到驗證碼。
TV channel¶
Enter— 將訊息內容複製到剪貼簿Alt+C— 抽出並只複製驗證碼Alt+R— 清快取並重新載入Alt+D— 刪除選中的訊息(會詢問確認)Ctrl+S— 切換來源:全部 ↔ 僅未讀- 每 5 秒自動刷新 — 等驗證碼時開著就好。
tv sms-archive 是同樣的選單,但查的是本機封存(完整歷史、可離線、不需登入路由器):
Enter / Alt+C 同上,Alt+R 先執行 sms archive 再重新載入,Ctrl+S 切換:全部 ↔ 僅驗證碼。
推播通知(sms watch)¶
HiLink 路由器沒有 webhook 或簡訊轉發功能,所以 watch 採「便宜的輪詢 + 推播」:
- 每
interval秒(預設 10)讀/api/sms/sms-count。B525s 上這個 endpoint 不需登入,所以平常輪詢不會佔用管理員 session,也不會把你踢出網頁 UI。 (若韌體要求登入,會退回每次輪詢短暫登入。) - 只有在
(LocalInbox, LocalUnread)改變時——或每full_sync秒保底一次——才登入、 讀最新 50 則,並與 platformdirs state 目錄中的seen.json比對 (Linux 為~/.local/state/sms/,macOS 為~/Library/Application Support/sms/)。 訊息以index|phone|date作為 key,因為 index 會被重複使用。 - 每則新訊息 → 一次 apprise 推播:標題
SMS from <phone>,偵測到驗證碼時內文前綴[code 123456]。同時印到 stdout(--json→ JSONL),並清掉sms快取, 讓sms/tv sms立刻看得到。
第一次執行會用目前收件匣靜默建立 state(不會一次灌出舊訊息);
--notify-existing-unread 則改為推播目前的未讀訊息。比建立時間點還舊、
又從未見過的訊息(例如刪除後滑回 50 則視窗的舊訊息)會被忽略。
目標(先符合者優先)— 設定於 ~/.config/sms/config.toml:
[watch]
notify_urls = ["tgram://<bot_token>/<chat_id>"] # 或 ntfy://topic、discord://…
# notify_tag = "sms" # …或使用 ~/.config/apprise/apprise.yaml 裡的 tag
SMS_NOTIFY_URLS(env)> notify_urls > notify_tag。URL 透過 APPRISE_URLS
環境變數交給 apprise,而非 argv,所以 token 不會出現在 ps。
Token 絕不進 chezmoi——只管理 .example。
推播失敗不會無聲無息:會記到 stderr,並附加到同一 state 目錄的 notify.log。
(該訊息仍會被標為已見——懷疑漏訊時查這個 log。)路由器 / 網路錯誤會以指數退避
(exponential backoff,上限 5 分鐘) 重試,而不是直接結束。
快速測試:
sms notify-test --dry-run # 列出 apprise 會通知哪些服務(不實際發送)
sms notify-test # 固定內容的測試推播
sms notify-test --latest # 最新一則真實簡訊,格式與 watch 完全相同
sms watch --once --no-notify # 建立 state
SMS_NOTIFY_URLS='macosx://' sms watch # 桌面橫幅,前景執行
Discord webhook 可直接使用:apprise 接受原生的
https://discord.com/api/webhooks/<id>/<token> 格式(等同 discord://<id>/<token>)。
請寫在 [watch] notify_urls,不要放在命令列——SMS_NOTIFY_URLS=… 前綴會留在 shell history。
以服務方式執行¶
在路由器 LAN 上一台常駐的 Linux 機器:
chezmoi apply # 部署 sms
$EDITOR ~/.config/sms/config.toml # 密碼 + [watch] notify_urls
sms watch --once --no-notify # 健全性檢查 + 建立 state
sms service install # 寫入 ~/.config/systemd/user/sms-watch.service 並 enable --now
journalctl --user -u sms-watch -f
sudo loginctl enable-linger "$USER" # 登出 / 重開機後仍持續執行(install 會在未開啟時提醒)
unit 是按需產生、而非由 chezmoi 部署,所以只會存在於選擇啟用的機器上。它會記下執行
install 時 shell 的 PATH(找 uv / apprise 用)。它不會繼承 shell 的 proxy
變數——若 apprise 需要 proxy 才能連到 Telegram 等,用 systemctl --user edit sms-watch
加上(Environment=HTTPS_PROXY=…)。對路由器的 LAN 流量一律繞過 proxy
(trust_env=False,與 mi-router / reyee 相同)。
macOS 上改用 tmux / pueue(或 LaunchAgent)執行 sms watch;sms service 僅支援 Linux。
收件匣容量 / prune¶
sms count 顯示 LocalInbox/LocalMax(網頁 UI 的「202/209」是未讀/總數,不是容量——
B525s 可存 500 則)。收件匣滿了路由器會丟棄新簡訊,所以 watch 在達到
capacity_warn(預設 95%)時推播一次警告。釋放空間:
prune 會先封存 (archive) 再刪除(見下節);--no-archive 則為有損刪除。
想完全自動,可讓 watch 處理(需主動開啟):
[watch]
archive = true
auto_prune_keep = 300 # 路由器最多只保留 300 則
auto_prune_include_unread = true # 推播送達後,路由器上的「未讀」已無意義
auto_prune_keep 在沒有 archive = true、或小於 50(watch 的比對視窗)時會拒絕執行,
所以自動清理永遠不會遺失訊息,也不會刪掉尚未推播的訊息。
封存與匯出¶
簡訊沒有像 email 那樣的標準(沒有對應 RFC 5322 / mbox / IMAP 的東西),所以封存用單純的 SQLite 檔,匯出則選兩種真的有工具生態的格式:
sms archive # 冪等 (idempotent):路由器上 M 則中新增 N 則
sms export -o sms.xml # SMS Backup & Restore XML(預設)
sms export --format mbox -o sms.mbox # 一則簡訊 = 一封信
sms export --format jsonl | jq … # 給腳本用
sms export --refresh -o sms.xml # 先封存再匯出
sqlite3 ~/.local/share/sms/archive.sqlite 'select date, phone, content from sms order by date desc limit 5'
| 格式 | 用什麼讀 |
|---|---|
sbr |
Android app SMS Backup & Restore(SyncTech)——可還原到手機簡訊資料庫;也有網頁檢視器與許多轉換工具。事實上的簡訊封存格式。 |
mbox |
任何郵件程式(Apple Mail / Thunderbird 匯入)、mutt -f、notmuch。主旨 = 前 70 字;Message-ID 固定,重複匯入會去重。 |
jsonl |
jq、腳本。 |
檢視封存¶
sms search 驗證碼 --since 2026-10 # 多個關鍵字為 AND 子字串比對
sms search --codes --limit 5 # 只看驗證碼簡訊
sms search --phone 68005
sms search --key 558e47 # 完整內容(key 前綴取自表格)
tv sms-archive # 模糊搜尋選單,複製內文 / 驗證碼
臨時探索可用機器上已有的工具:
| 工具 | 指令 | 適合 |
|---|---|---|
VisiData(vd) |
vd ~/.local/share/sms/archive.sqlite |
瀏覽 / 排序 / 正規表示式篩選(\|)/ 次數統計(Shift+F) |
| sqlit | sqlit --db-type sqlite --file-path ~/.local/share/sms/archive.sqlite |
在 TUI 中寫 SQL |
| sqlite3 | sqlite3 -box …/archive.sqlite "select phone, count(*) n from sms group by phone order by n desc limit 10" |
一行查詢 |
| datasette | uvx datasette ~/.local/share/sms/archive.sqlite |
瀏覽器介面——只開在 localhost(封存內含驗證碼) |
封存檔位於 platformdirs 的 data 目錄(Linux 為 ~/.local/share/sms/archive.sqlite,
或 XDG_DATA_HOME 指向處),權限 600。每列的 key 與 watch 的 seen-state 相同
(index|phone|date);unread 欄位記錄最後一次封存時路由器上的已讀狀態。
路由器時間戳是不帶時區的本地時間,匯出時假設為本機時區。
剪貼簿¶
sms code 會 pipe 到既有的 x copy helper(見 dot_dotfiles/bin/executable_x),
所以剪貼簿在 macOS(pbcopy)、Linux Wayland(wl-copy)、Linux X11
(xclip/xsel)、WSL(clip.exe)以及透過 SSH(OSC 52)都能一致運作。
快取¶
收件匣預設快取在 ~/.cache/sms/inbox.json,TTL 30 秒。
這是為了避免連續呼叫 sms 時一直重新登入路由器(每次新登入都會讓路由器
原有的 session 失效,會把使用者踢出網頁 UI)。要繞過時用 sms refresh
或 sms --no-cache <cmd>。
排錯¶
router error: ResponseErrorLoginCsrfException— 韌體要求新的 SCRAM challenge。執行sms refresh然後重試。login failed: 108003 — user already logged in— 從網頁 UI 登出, 或等約 5 分鐘讓路由器 session 過期。- 連不上 — 確認你在路由器的 LAN 上(或透過 Tailscale / SSH port-forward 隧道進去)。API 預設不會在 WAN 暴露。
sms watch執行中,網頁 UI 登入出現 108003 — watcher 只在收件匣變動時短暫登入, 幾秒後重試即可;或在使用網頁 UI 時systemctl --user stop sms-watch。sms watch偶爾出現RemoteDisconnected— 路由器的 httpd 會提早關閉 keep-alive 連線。輪詢改用單一 GET 加Connection: close,單次失敗會在 2 秒後靜默重試; 只有連續失敗才會記錄並退避。sms watch沒有推播 — 查看 state 目錄中的notify.log,並直接測試 URL:apprise -b test '<url>'。- host 錯了 — 許多 HiLink 路由器預設是
192.168.8.1;這台用192.168.168.1。用SMS_ROUTER_HOST覆寫。