================================================================ WebSocket 即時通訊 API — 使用說明書 服務位址:https://api2.infotensor.com/wsapi 版本:v1.0.0  最後更新:2026-03-28 ================================================================ 目錄 ---- 1. 系統概述 2. 快速開始 3. 身份驗證(JWT) 4. Session 管理 API 5. WebSocket 連線 6. 訊息格式說明 7. 完整通訊流程範例 8. 錯誤代碼一覽 9. 連線限制與限流規則 10. 前端重連策略建議 11. 健康檢查端點 12. 附錄:完整 JavaScript 範例 ================================================================ 1. 系統概述 ================================================================ 本服務提供基於 WebSocket 的即時一對一通訊能力,具備以下特性: • JWT 身份驗證:所有連線與 API 均需攜帶有效 token • Session 授權:通訊雙方須先建立授權 Session,防止任意用戶互傳訊息 • 訊息持久化:每則訊息寫入 PostgreSQL,可查詢歷史紀錄 • Idempotency:相同 idempotency_key 的訊息只處理一次,安全重送不重複 • ACK 機制:接收方確認收到後伺服器記錄時間,可追蹤送達狀態 • Rate Limit:每條 WebSocket 連線每分鐘最多 60 則訊息 • Heartbeat:伺服器每 20 秒發送 ping,60 秒無回應自動斷線 • 跨節點路由:Redis pub/sub 支援多伺服器節點部署 技術規格: 協定 HTTPS / WSS(TLS 1.2+) 認證 JWT HS256 訊息格式 JSON(UTF-8) 最大訊息 64 KB ================================================================ 2. 快速開始 ================================================================ 步驟一:取得 JWT Token (正式環境由您的 Auth Service 簽發;開發測試可使用以下端點) GET https://api2.infotensor.com/wsapi/token/test?user_id=<你的用戶ID> 回應範例: { "token": "eyJhbGci...", "user_id": "alice", "ws_url": "wss://api2.infotensor.com/wsapi/ws/connect?token=eyJhbGci...&session_id=" } 步驟二:建立 Session 由發起方(user_a)呼叫,取得 session_id POST https://api2.infotensor.com/wsapi/sessions Authorization: Bearer 步驟三:對方接受 Session 由接收方(user_b)呼叫,Session 狀態變為 active POST https://api2.infotensor.com/wsapi/sessions/{session_id}/accept Authorization: Bearer 步驟四:建立 WebSocket 連線 雙方均可連線,token 與 session_id 透過 query string 傳入 wss://api2.infotensor.com/wsapi/ws/connect?token=&session_id= 步驟五:收發訊息 連線後即可雙向傳送 JSON 格式的訊息 ================================================================ 3. 身份驗證(JWT) ================================================================ ─── 3.1 Token 格式 ─────────────────────────────────────────── 所有 HTTP API 使用 Authorization Header 傳入 token: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... WebSocket 連線因瀏覽器限制無法自訂 Header,改用 query string: wss://...?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ─── 3.2 Token Claims 結構 ──────────────────────────────────── { "sub": "user-uuid", // 用戶唯一 ID(字串) "iat": 1711612800, // 簽發時間(Unix timestamp) "exp": 1711616400 // 過期時間(預設 1 小時後) } ─── 3.3 Token 過期處理 ─────────────────────────────────────── HTTP API: Token 過期時回傳 HTTP 401,detail 為 "token_expired" 請重新登入取得新 token 後再呼叫 WebSocket 連線: Token 過期時握手階段即被拒絕(HTTP 403),連線無法建立 已建立的連線不會因 token 過期而中斷,過期只影響新連線 ================================================================ 4. Session 管理 API ================================================================ ─── 4.1 建立 Session ───────────────────────────────────────── POST /wsapi/sessions Content-Type: application/json Authorization: Bearer 說明: 由發起方(user_a)呼叫。建立後 Session 狀態為 "pending", 等待另一方接受。user_a 的身份從 JWT token 自動取得, 無需在 request body 傳入。 Request Body:無(空 body 或 {}) Response 200: { "id": "d8356bec-51d5-45eb-b8fa-1bf1e13d582f", "user_a": "alice", "user_b": null, "status": "pending", "created_at": "2026-03-28T08:29:08Z", "accepted_at": null, "closed_at": null } ─── 4.2 接受 Session ───────────────────────────────────────── POST /wsapi/sessions/{session_id}/accept Authorization: Bearer 說明: 由接收方(user_b)呼叫。呼叫後 Session 狀態變為 "active", 雙方即可透過 WebSocket 傳送訊息。 Response 200: { "id": "d8356bec-51d5-45eb-b8fa-1bf1e13d582f", "user_a": "alice", "user_b": "bob", "status": "active", "created_at": "2026-03-28T08:29:08Z", "accepted_at": "2026-03-28T08:29:10Z", "closed_at": null } ─── 4.3 關閉 Session ───────────────────────────────────────── POST /wsapi/sessions/{session_id}/close Authorization: Bearer 說明: user_a 或 user_b 均可呼叫。關閉後 Session 狀態變為 "closed", 不再接受任何新訊息。已連線的 WebSocket 不會自動斷線, 但後續送出的訊息會收到 unauthorized 錯誤。 Response 200: { "id": "d8356bec-51d5-45eb-b8fa-1bf1e13d582f", "user_a": "alice", "user_b": "bob", "status": "closed", "created_at": "2026-03-28T08:29:08Z", "accepted_at": "2026-03-28T08:29:10Z", "closed_at": "2026-03-28T09:00:00Z" } ─── 4.4 Session 狀態機 ─────────────────────────────────────── [建立] [accept] [close] pending ──► active ──► closed ▲ │(任何時候均可關閉) └───────────────── ================================================================ 5. WebSocket 連線 ================================================================ ─── 5.1 連線端點 ────────────────────────────────────────────── wss://api2.infotensor.com/wsapi/ws/connect 必要 Query Parameters: token JWT token(同 HTTP API 使用的 token) session_id 已建立且狀態為 active 的 Session ID 完整範例: wss://api2.infotensor.com/wsapi/ws/connect ?token=eyJhbGci... &session_id=d8356bec-51d5-45eb-b8fa-1bf1e13d582f ─── 5.2 連線成功事件 ───────────────────────────────────────── 連線建立後,伺服器會立即推送一則 connected 事件: { "type": "connected", "data": { "conn_id": "213e8138-3731-4a78-b779-91b52b1b4698" } } conn_id 為本次連線的唯一識別碼,可用於除錯與日誌追蹤。 ─── 5.3 連線限制 ───────────────────────────────────────────── • 同一用戶可建立多條連線(多裝置支援) • 每條連線的 session_id 可以相同或不同 • Token 過期不影響已建立的連線(只影響新連線) ================================================================ 6. 訊息格式說明 ================================================================ ─── 6.1 用戶端 → 伺服器:傳送訊息 ─────────────────────────── type 欄位為 "message"(可省略,預設即為 message) { "type": "message", "message_id": "71b75205-9d7f-494f-b0fb-49bd98ef264d", "session_id": "d8356bec-51d5-45eb-b8fa-1bf1e13d582f", "sender_user_id": "alice", "recipient_user_id":"bob", "timestamp": "2026-03-28T08:30:00Z", "idempotency_key": "unique-key-001", "payload": { "text": "你好!" } } 欄位說明: message_id 建議使用 UUID v4,作為訊息唯一識別碼 若省略,伺服器自動產生 session_id 必填,須與連線時的 session_id 相同 sender_user_id 必填,須與 JWT token 中的 sub 一致 recipient_user_id 必填,須為同 Session 的另一方 timestamp 訊息產生時間(ISO 8601 格式) 建議使用 UTC 時間 idempotency_key 必填,長度 1~64 字元 相同 key 的訊息只會被處理一次 建議格式:UUID 或「用戶ID-時間戳-序號」 payload 必填,任意 JSON 物件 可放入 text、圖片 URL、自訂資料等 ─── 6.2 用戶端 → 伺服器:確認收到(ACK)──────────────────── 收到伺服器轉發的訊息後,回送 ACK 更新送達狀態: { "type": "ack", "message_id": "71b75205-9d7f-494f-b0fb-49bd98ef264d" } ─── 6.3 用戶端 → 伺服器:Ping ─────────────────────────────── 用戶端可主動送出 ping 維持連線活躍(亦可不送,伺服器會自動 ping): { "type": "ping" } ─── 6.4 伺服器 → 用戶端:收到訊息 ─────────────────────────── 當對方傳送訊息時,伺服器推送給本用戶: { "type": "message", "message_id": "71b75205-9d7f-494f-b0fb-49bd98ef264d", "session_id": "d8356bec-51d5-45eb-b8fa-1bf1e13d582f", "sender_user_id": "alice", "recipient_user_id":"bob", "timestamp": "2026-03-28T08:30:00Z", "idempotency_key": "unique-key-001", "payload": { "text": "你好!" } } ─── 6.5 伺服器 → 用戶端:Pong ─────────────────────────────── 收到 ping 後伺服器回應: { "type": "pong" } ─── 6.6 伺服器 → 用戶端:伺服器 Ping ──────────────────────── 伺服器每 20 秒主動推送一次 ping(heartbeat): { "type": "ping" } 收到後建議回送 ping 或任何訊息以更新 liveness 計時器。 若 60 秒內沒有任何通訊,伺服器會主動關閉連線(Close Code 1001)。 ─── 6.7 伺服器 → 用戶端:錯誤事件 ─────────────────────────── { "type": "error", "data": { "code": "unauthorized", "detail": "user alice → charlie not in session xxx" } } 錯誤不會中斷連線,訊息被拒絕後連線持續有效。 ================================================================ 7. 完整通訊流程範例 ================================================================ 情境:Alice 傳訊息給 Bob ┌──────────┐ ┌──────────────┐ ┌──────────┐ │ Alice │ │ WebSocket │ │ Bob │ │ (前端) │ │ Server │ │ (前端) │ └────┬─────┘ └──────┬───────┘ └────┬─────┘ │ │ │ │ POST /sessions │ │ │ ─────────────────────────► │ │ │ ◄── {id, status:pending} │ │ │ │ │ │ (將 session_id 傳給 Bob) │ │ │ ─────────────────────────────┼──────────────────────────► │ │ │ POST /sessions/{id}/accept │ │ │ ◄───────────────────────── │ │ │ ──► {status:active} │ │ │ │ │ WS connect?session_id=... │ WS connect?session_id=... │ │ ─────────────────────────► │ ◄──────────────────────── │ │ ◄── {type:connected} │ ──► {type:connected} │ │ │ │ │ {type:message, payload:...} │ │ │ ─────────────────────────► │ │ │ │ {type:message, payload:...}│ │ │ ────────────────────────► │ │ │ │ │ │ {type:ack, message_id:...} │ │ │ ◄──────────────────────── │ │ │ (DB 更新 ack_at) │ │ │ │ Idempotency 說明: 若 Alice 的訊息因網路問題未收到伺服器回應,可以用相同的 idempotency_key 重送。伺服器會辨識重複並跳過持久化, 但仍會嘗試路由給 Bob(若 Bob 尚未收到)。 ================================================================ 8. 錯誤代碼一覽 ================================================================ ─── HTTP API 錯誤 ──────────────────────────────────────────── HTTP 401 token_invalid Token 格式錯誤或簽名不符 HTTP 401 token_expired Token 已過期,請重新登入 HTTP 401 missing_token 未攜帶 Authorization Header HTTP 400 <業務錯誤> Session 狀態不符(如已非 pending) HTTP 503 database_unavailable 資料庫暫時不可用 ─── WebSocket 握手錯誤(HTTP 403)────────────────────────── 連線被拒絕(握手失敗),原因為 token 無效或過期。 請確認 token 有效後再重新連線。 ─── WebSocket 應用層錯誤(連線維持,訊息被拒)──────────────── code: invalid_json 收到非 JSON 格式的訊息 code: invalid_message 訊息欄位格式錯誤(缺少必填欄位等) code: unauthorized Session 狀態非 active,或用戶不在 Session 中 code: rate_limited 超過每分鐘 60 則的限流門檻 code: server_error 伺服器內部錯誤(持久化或路由失敗) ─── WebSocket Close Code ────────────────────────────────── 1000 正常關閉(主動呼叫 close) 1001 Going Away(伺服器 heartbeat 超時主動關閉) 4001 JWT 過期(建立連線時) 4003 未授權 4029 Rate limit 超限(極端情況下關閉連線) 4500 伺服器內部錯誤 ================================================================ 9. 連線限制與限流規則 ================================================================ WebSocket 訊息限流: 每條連線每分鐘最多 60 則訊息 超過後收到 error(code: rate_limited),訊息被丟棄 限流計數每 60 秒自動重置 HTTP API 限流: 每個 IP 每分鐘最多 120 次請求 超過後回傳 HTTP 429 訊息大小限制: 單則訊息最大 64 KB(含所有 JSON 欄位) Heartbeat 超時: 20 秒一次 ping,60 秒內無任何通訊即強制關閉連線 client 端只要有傳任何訊息(包括 ping)即可維持連線 ================================================================ 10. 前端重連策略建議 ================================================================ 建議使用指數退避(Exponential Backoff)重連: 初始延遲 1 秒 最大延遲 30 秒 退避倍率 2 倍 加入隨機抖動防止雪崩效應 重連時機: • WebSocket onclose 事件觸發(任何 close code) • WebSocket onerror 事件觸發 • 網路重新連線後(navigator.onLine 事件) 重連前需確認: • Token 是否仍有效(檢查 exp claim) • Token 若過期,先刷新 token 再重連 • Session 是否仍為 active(可快取狀態,重連後驗證) 範例邏輯: let retryDelay = 1000 const MAX_DELAY = 30000 function connect() { ws = new WebSocket(url) ws.onclose = (event) => { if (event.code === 4001) { // token 過期,先刷新再重連 refreshToken().then(connect) return } const jitter = Math.random() * 1000 setTimeout(connect, retryDelay + jitter) retryDelay = Math.min(retryDelay * 2, MAX_DELAY) } ws.onopen = () => { retryDelay = 1000 // 重連成功後重設延遲 } } Heartbeat 維持(建議): 收到伺服器 ping 後回送 ping,確保 liveness 計時器被更新: ws.onmessage = (event) => { const data = JSON.parse(event.data) if (data.type === 'ping') { ws.send(JSON.stringify({ type: 'ping' })) } } ================================================================ 11. 健康檢查端點 ================================================================ Liveness(存活檢查): GET https://api2.infotensor.com/wsapi/health/live 回應(200 OK): { "status": "ok" } 說明:只要服務 process 存活即回傳 200, 適用於 Kubernetes liveness probe。 Readiness(就緒檢查): GET https://api2.infotensor.com/wsapi/health/ready 回應(200 OK,所有依賴正常): { "status": "ok", "checks": { "redis": "ok" } } 回應(503,部分依賴異常): { "status": "degraded", "checks": { "redis": "error: Connection refused" } } 說明:Redis 可用時才回傳 200, 適用於 Kubernetes readiness probe 與 nginx upstream 健康檢查。 Swagger API 文件: GET https://api2.infotensor.com/wsapi/docs ================================================================ 12. 附錄:完整 JavaScript 範例 ================================================================ 以下為瀏覽器端可直接使用的完整範例: ───────────────────────────────────────────────────────────── const API_BASE = 'https://api2.infotensor.com/wsapi' class WsApiClient { constructor(token) { this.token = token this.ws = null this.sessionId = null this.retryDelay = 1000 this.handlers = {} } // ── Step 1: 建立 Session(由發起方呼叫)────────────────── async createSession() { const res = await fetch(`${API_BASE}/sessions`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.token}` } }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const session = await res.json() this.sessionId = session.id return session } // ── Step 2: 接受 Session(由接收方呼叫)───────────────── async acceptSession(sessionId) { const res = await fetch(`${API_BASE}/sessions/${sessionId}/accept`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.token}` } }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const session = await res.json() this.sessionId = session.id return session } // ── Step 3: 連線 WebSocket ──────────────────────────────── connect() { const url = `wss://api2.infotensor.com/wsapi/ws/connect` + `?token=${this.token}` + `&session_id=${this.sessionId}` this.ws = new WebSocket(url) this.ws.onopen = () => { console.log('WebSocket 已連線') this.retryDelay = 1000 } this.ws.onmessage = (event) => { const data = JSON.parse(event.data) switch (data.type) { case 'connected': console.log('連線 ID:', data.data.conn_id) break case 'message': // 收到對方訊息 if (this.handlers.onMessage) { this.handlers.onMessage(data) } // 回送 ACK this.ack(data.message_id) break case 'ping': // 回送 ping 維持 heartbeat this.ws.send(JSON.stringify({ type: 'ping' })) break case 'pong': // 伺服器確認 ping break case 'error': console.error('伺服器錯誤:', data.data) if (this.handlers.onError) { this.handlers.onError(data.data) } break } } this.ws.onclose = (event) => { console.log(`連線關閉,代碼: ${event.code}`) if (event.code === 4001) { // Token 過期,需刷新後再重連 if (this.handlers.onTokenExpired) { this.handlers.onTokenExpired() } return } // 自動重連(指數退避) const jitter = Math.random() * 500 setTimeout(() => this.connect(), this.retryDelay + jitter) this.retryDelay = Math.min(this.retryDelay * 2, 30000) } this.ws.onerror = (err) => { console.error('WebSocket 錯誤', err) } } // ── 傳送訊息 ────────────────────────────────────────────── sendMessage(recipientUserId, payload) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { throw new Error('WebSocket 未連線') } const message = { type: 'message', message_id: crypto.randomUUID(), session_id: this.sessionId, sender_user_id: '<你的 user_id>', // 填入登入用戶的 ID recipient_user_id: recipientUserId, timestamp: new Date().toISOString(), idempotency_key: crypto.randomUUID(), // 建議每則訊息產生新的 payload: payload } this.ws.send(JSON.stringify(message)) return message.message_id } // ── 送出 ACK ────────────────────────────────────────────── ack(messageId) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ack', message_id: messageId })) } } // ── 關閉 Session ────────────────────────────────────────── async closeSession() { await fetch(`${API_BASE}/sessions/${this.sessionId}/close`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.token}` } }) if (this.ws) this.ws.close(1000) } // ── 事件處理器設定 ──────────────────────────────────────── on(event, handler) { this.handlers[event] = handler return this } } // ── 使用範例:Alice 發起通訊 ───────────────────────────────── const alice = new WsApiClient('Alice的JWT_TOKEN') alice .on('onMessage', (msg) => { console.log('收到訊息:', msg.payload) }) .on('onError', (err) => { console.error('錯誤:', err.code, err.detail) }) // 1. 建立 session const session = await alice.createSession() console.log('Session ID:', session.id) // 2. 將 session.id 傳給 Bob(透過其他管道,如推播通知) // 3. 連線 alice.connect() // 4. 傳送訊息 alice.sendMessage('bob', { text: '嗨 Bob!' }) // ── 使用範例:Bob 接受通訊 ─────────────────────────────────── const bob = new WsApiClient('Bob的JWT_TOKEN') bob.on('onMessage', (msg) => { console.log('Bob 收到:', msg.payload) }) // 1. 接受 Alice 的 session await bob.acceptSession(session.id) // 2. 連線 bob.connect() ───────────────────────────────────────────────────────────── ================================================================ 如有問題,請至 https://api2.infotensor.com/wsapi/docs 查看互動式 API 文件(Swagger UI) ================================================================