EN | 简 | 繁
# Token Monitor Hub — Cloudflare Worker
> 屬於 **[Token Monitor](https://github.com/Javis603/token-monitor)** 專案。這個目錄只是 Cloudflare Worker hub;桌面小工具、無頭 agent 和完整文件都在主倉庫。一鍵部署會建立一份獨立副本,不會自動更新,所以請回主倉庫查看新版本。
自架 Node hub 的即插即用替代品,以 Cloudflare Worker 部署,用 Durable Object 保存裝置狀態。它講的是同一套 HTTP 協定(`/api/ingest`、`/api/stats`、`/api/stats/stream`),所以小工具和 agent 無需改動即可使用,只是 Hub URL 不同。
相比 Node hub,用它的理由:
- 不需要一直開機的機器,Cloudflare 幫你跑。
- 預設公開 HTTPS。跨網路可達,也能被 iOS 小工具(Widgy、Scriptable)存取。
- 免費額度輕鬆涵蓋小團隊的用量。
## 前置條件
- Cloudflare 帳號(免費)。
- Node.js 22+(Wrangler v4 要求 `>=22.0.0`)。
## 部署
```bash
cd worker
npm install
npx wrangler login # one-time browser auth
npx wrangler secret put TOKEN_MONITOR_SECRET # paste a long random string
npx wrangler deploy
```
Wrangler 會印出部署後的 URL,例如:
```
https://token-monitor-hub..workers.dev
```
把每個 agent 和小工具都指向這個 URL。
### 一鍵部署故障排除
Cloudflare 的 **Deploy to Cloudflare** 按鈕很方便,但一直有兩種間歇性故障:
- **部署頁報「無法解析 Wrangler 配置文件」**——從 `worker/` 子目錄讀取設定時出的
岔子。
- **部署後的 Worker 只回傳純文字 `Hello world`**——踩中了 Cloudflare 的已知匯入
故障,產生的倉庫*沒有* Worker 原始碼(只有 `README.md` + `wrangler.toml`)。它會
顯示成功,但背後根本沒有程式碼。把 Workers Builds 重新連到包含完整 `worker/` 目錄
的倉庫,或直接改用手動部署。
兩者都是 CF 側的問題。故障在同一個瀏覽器工作階段裡往往有「黏性」,所以先用無痕/
隱私視窗(或換個瀏覽器)重開部署連結再試。若仍失敗,就跳過按鈕——上面的手動
`cd worker && npx wrangler deploy` 一定會成:同一份程式碼,只是少了 CF 那個不穩定的
匯入步驟。
## 本機開發
```bash
npm run dev # wrangler dev — local Worker with a real Durable Object
```
各端點與正式環境行為一致。如有需要,用 `wrangler secret put TOKEN_MONITOR_SECRET --env dev` 設定一個獨立的開發密鑰。
## 設定小工具
設定 → Multi-device Sync:
- Hub URL:`https://token-monitor-hub..workers.dev`
- Secret:你用 `wrangler secret put` 設定的值
儲存。SSE 串流連上後,狀態標籤會從 `Local` 切換為 `Live`。
## 設定 agent
可以透過專案根目錄的 `.env`(從 `.env.example` 複製):
```env
TOKEN_MONITOR_HUB_URL=https://token-monitor-hub..workers.dev
TOKEN_MONITOR_SECRET=
TOKEN_MONITOR_DEVICE_ID= # optional — defaults to hostname
```
或在啟動時內聯匯出:
```bash
TOKEN_MONITOR_HUB_URL=https://token-monitor-hub..workers.dev \
TOKEN_MONITOR_SECRET= \
npm run agent
```
## 透過 Widgy 或 Scriptable 在 iPhone 上使用
Worker 以開放 CORS 的純 JSON 暴露 `GET /api/stats`,所以 iOS 小工具執行環境可以直接呼叫。當 `PUBLIC_STATS_ENABLED=1` 時,它也能暴露無需驗證的 `GET /api/public/stats` 供公開儀表板使用;該回應會省略每裝置記錄和帳號識別。
開啟公開端點:
```bash
npx wrangler secret put PUBLIC_STATS_ENABLED # enter 1
```
不設定則保持 `/api/public/stats` 關閉。
### Widgy
選 **async / no main()** 範本。使用 `?secret=` 查詢字串驗證:Widgy 那個隱藏的 WKWebView 可能會卡在 `Authorization: Bearer` 觸發的 CORS 預檢上,而 URL 會留在裝置本機的 Widgy 設定裡,所以密鑰不會進入外部日誌。
最簡版本,只要一個數字:
```js
const HUB = 'https://token-monitor-hub..workers.dev';
const SECRET = '';
const url = HUB + '/api/stats?secret=' + SECRET;
fetch(url)
.then(r => r.json())
.then(s => sendToWidgy(
Number(s.periods.today.totalTokens).toLocaleString('en-US')
))
.catch(e => sendToWidgy('err:' + e.message));
```
改最後那行 `sendToWidgy(...)` 即可選擇任意欄位:
| 想顯示 | 把內層運算式替換為 |
|--------------------|------------------------------------------------------------|
| 今日 tokens | `Number(s.periods.today.totalTokens).toLocaleString('en-US')` |
| 今日成本 | `'$' + s.periods.today.costUsd.toFixed(2)` |
| 本月 tokens | `Number(s.periods.month.totalTokens).toLocaleString('en-US')` |
| 本月成本 | `'$' + s.periods.month.costUsd.toFixed(2)` |
| 累計 tokens | `Number(s.periods.allTime.totalTokens).toLocaleString('en-US')` |
| 累計成本 | `'$' + s.periods.allTime.costUsd.toFixed(2)` |
帶設定區塊、並對 token 做精簡 `K / M / B` 格式化的組合版本,適合窄小工具版面:
```js
const HUB = 'https://token-monitor-hub..workers.dev';
const SECRET = '';
const PERIOD = 'today'; // 'today' | 'month' | 'allTime'
const SHOW = 'tokens+cost'; // 'tokens' | 'cost' | 'tokens+cost'
function fmtTokens(n) {
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
return String(Math.round(n));
}
function fmtCost(n) {
return '$' + (n >= 10 ? n.toFixed(2) : n.toFixed(4));
}
fetch(HUB + '/api/stats?secret=' + SECRET)
.then(r => r.json())
.then(stats => {
const p = stats.periods[PERIOD] || { totalTokens: 0, costUsd: 0 };
const t = fmtTokens(p.totalTokens || 0);
const c = fmtCost(p.costUsd || 0);
if (SHOW === 'tokens') sendToWidgy(t);
else if (SHOW === 'cost') sendToWidgy(c);
else sendToWidgy(t + ' · ' + c);
})
.catch(e => sendToWidgy('err:' + e.message));
```
每個 Widgy 文字元素各用一個腳本:複製它並修改 `PERIOD` / `SHOW` 來驅動不同欄位。
### Scriptable
```js
const req = new Request('https://token-monitor-hub..workers.dev/api/stats');
req.headers = { authorization: 'Bearer ' };
const stats = await req.loadJSON();
const todayTokens = stats.periods.today.totalTokens;
```
iOS 小工具沒有省電的推送通道,所以執行環境會自行每隔幾分鐘重新拉取。
## Stats 回應結構
`GET /api/stats` 回傳聚合快照。挑你小工具需要的欄位即可:
```jsonc
{
"updatedAt": "2026-05-18T18:02:19.459Z",
"periods": {
"today": { /* see PeriodSummary below */ },
"month": { /* ... */ },
"allTime": { /* ... */ }
},
"limits": {
"updatedAt": "2026-05-18T18:02:19.459Z",
"providers": [
{
"provider": "claude",
"accountKey": "sha256:...",
"sourceDeviceId": "macbook",
"stale": false,
"status": "ok",
"windows": [
{ "kind": "session", "usedPercent": 42, "remainingPercent": 58, "resetsAt": "2026-05-18T21:00:00.000Z" },
{ "kind": "weekly", "usedPercent": 20, "remainingPercent": 80, "resetsAt": "2026-05-25T00:00:00.000Z" }
]
}
]
},
"devices": [
{
"deviceId": "macbook",
"hostname": "macbook.local",
"platform": "darwin-arm64",
"updatedAt": "2026-05-18T18:01:50.000Z",
"receivedAt": "2026-05-18T18:01:51.012Z",
"ageMs": 28447,
"stale": false,
"periods": { "today": {...}, "month": {...}, "allTime": {...} }
}
]
}
```
`PeriodSummary`:
```jsonc
{
"totalTokens": 1234567, // sum across all devices for this period
"costUsd": 12.345678,
"clients": { "claude": 800000, "codex": 400000, "hermes": 34567 },
"clientCosts": { "claude": 8.12, "codex": 4.10, "hermes": 0.12 },
"models": { "claude-opus-4-7": 600000, "gpt-5-thinking-medium": 400000, ... },
"modelCosts": { "claude-opus-4-7": 7.50, "gpt-5-thinking-medium": 4.00, ... }
}
```
如果想做每裝置的小工具,每個裝置也在 `devices[i].periods[period]` 下帶有自己的每週期數值。當某裝置的 `receivedAt` 早於 `STALE_AFTER_MS`(預設 10 分鐘)時會被標記為 `stale: true`,想顯示「離線」狀態時很有用。
`limits.providers` 依 provider 帳號聚合。帶驗證的 stats 端點包含用於去重的帳號雜湊。開啟後,`/api/public/stats` 會剝除這些雜湊、標籤、來源裝置 id,以及完整的 `devices` 清單。
## 端點
| 方法 | 路徑 | 驗證 | 說明 |
|--------|----------------------------|--------|--------------------------------------------|
| GET | `/api/health` | 無 | 存活探針 + 裝置數 |
| GET | `/api/public/stats` | 無 | `PUBLIC_STATS_ENABLED=1` 時提供不含 devices/帳號 id 的公開聚合統計 |
| GET | `/api/stats` | 密鑰 | 聚合統計(today / month / allTime) |
| GET | `/api/stats/stream` | 密鑰 | SSE 串流,每次 ingest 都推送 |
| GET | `/api/devices` | 密鑰 | 原始的每裝置記錄 |
| POST | `/api/ingest` | 密鑰 | 更新某個裝置的用量摘要 |
| DELETE | `/api/devices/{deviceId}` | 密鑰 | 刪除一筆裝置記錄 |
密鑰有三種接受方式(任一即可):
1. `Authorization: Bearer `:agent、小工具,以及任何伺服器 / 桌面客戶端首選。
2. `x-token-monitor-secret: `:無法設定 `Authorization` 的客戶端的後備方案。
3. `?secret=` 查詢字串:針對 iOS 小工具執行環境(Widgy、Scriptable)的變通方案,它們的 WKWebView 難以處理 `Authorization` 標頭的 CORS 預檢。只在 URL 留在裝置本機的客戶端上使用。
密鑰是必需的。當 `TOKEN_MONITOR_SECRET` 未設定時,所有資料路由都回傳 `503 secret_required`,只有 `/api/health` 和可選開啟的 `/api/public/stats` 會回應。請在部署前(或部署時)設定它。
## 儲存與成本
裝置記錄存放在 Durable Object 的 SQLite 儲存裡,以 `dev:` 為鍵。每次 ingest 寫一列;讀取時在記憶體中聚合所有列。對於小團隊(≤ 10 台裝置,每台每分鐘一次 ingest),這遠在免費方案限額之內:
- Worker 請求:≤ 15k/天,免費額度為 100k/天
- DO SQL 讀/寫:≤ 30k/天,免費額度為數百萬
- DO 儲存:總共幾 KB
## Tail 日誌
```bash
npm run tail
```
即時串流輸出 Worker + Durable Object 日誌。