WebSocket 連線與認證
目錄
連線資訊
| 項目 | 值 |
|---|---|
| 端點 | wss://vas-poc.vurbo.ai/ws |
| 協定 | WebSocket |
| 資料格式 | JSON |
| 認證方式 | Ticket(見下方) |
認證方式
VAS WebSocket 使用 Ticket 機制 進行認證,透過 Sec-WebSocket-Protocol 傳遞一次性 Ticket。詳細說明請參考 認證機制。
步驟 1:取得 Ticket
使用 API Key 向 REST API 換取一次性 Ticket:
POST /api/v1/auth/ticket
X-API-Key: vas_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
回應:
{
"ticket": "aBcDeFgHiJkLmNoPqRsTuVwXyZ012345",
"expires_in": 60
}
| 欄位 | 類型 | 說明 |
|---|---|---|
ticket | string | 一次性 Ticket(32 字元) |
expires_in | int | 有效期(秒) |
步驟 2:使用 Ticket 連線 WebSocket
將 Ticket 放入 Sec-WebSocket-Protocol,格式為 ticket.{TICKET_VALUE}:
// 瀏覽器原生支援
const ws = new WebSocket('wss://vas-poc.vurbo.ai/ws', [`ticket.${ticket}`]);
ws.onopen = () => {
console.log('Connected! Protocol:', ws.protocol);
// 開始使用 WebSocket...
};
ws.onerror = (error) => {
console.error('Connection failed:', error);
};
Node.js 範例:
const WebSocket = require('ws');
const ws = new WebSocket('wss://vas-poc.vurbo.ai/ws', [`ticket.${ticket}`]);
Ticket 特性
| 特性 | 說明 |
|---|---|
| 有效期 | 60 秒 |
| 使用次數 | 僅能使用一次(使用後立即刪除) |
| 安全性 | API Key 不會暴露在 WebSocket 連線中 |
| 防重放攻擊 | 使用原子操作確保一次性 |
Ticket 錯誤碼
| 錯誤碼 | HTTP 狀態碼 | 說明 |
|---|---|---|
ticket_invalid | 401 | Ticket 無效或已過期 |
ticket_expired | 401 | Ticket 已過期 |
ticket_already_used | 401 | Ticket 已被使用 |
ticket_validation_failed | 500 | Ticket 驗證失敗 |
完整 API 規格請參考 Auth Ticket API。
訊息格式
所有訊息使用統一的巢狀結構:
{
"type": "服務類型",
"data": { ... }
}
單則訊息大小上限
單則 WebSocket 訊息有大小上限(預設 1 MB,可依環境調整)。超過時連線會直接被關閉(close code 1009),不會送出任何錯誤訊息——客戶端只會看到連線莫名中斷。
一般使用不會接近這個上限:建議的音訊 frame 為 100 毫秒(約 4 KB),即使一次送 1 秒也只有數十 KB。
注意:唯一可能撞到的是大型字庫。三個字庫區塊可以分多次
config送出——未帶的區塊不受影響、維持原值,所以拆開送是安全的。若連線在送出大型config後立刻中斷且沒有任何錯誤訊息,請往這個方向排查。
服務類型
| type | 說明 |
|---|---|
health | 心跳機制 |
voice-translation | 語音翻譯服務 |
error | 錯誤訊息 |
錯誤訊息格式
當發生錯誤時,伺服器會回傳 type: "error" 的訊息:
{
"type": "error",
"data": {
"error_code": "auth_invalid_api_key",
"severity": "fatal",
"message": "API Key 無效",
"context": "auth",
"request_id": "req_abc123xyz789",
"timestamp": "2026-01-15T10:30:45.123Z"
}
}
| 欄位 | 類型 | 說明 |
|---|---|---|
error_code | string | 錯誤碼(程式化處理用) |
severity | string | 嚴重程度:fatal / error / warning |
message | string | 人類可讀的錯誤訊息 |
context | string | 錯誤來源分類 |
request_id | string | 請求追蹤 ID |
timestamp | string | 錯誤發生時間(ISO 8601) |
完整錯誤碼列表請參考 錯誤碼參考。
心跳機制(Health)
功能說明
用於確認 WebSocket 連線是否正常。建議每 30 秒發送一次 ping,若未收到 pong 則視為斷線並重連。上一場錄音結束後仍在處理中時(例如正在產生摘要),pong 可能延後數秒到數十秒;請保留足夠的等待時間,不要只因暫時沒有收到 pong 就判定斷線。
使用場景
- 維持長時間連線
- 檢測連線狀態
- 防止連線逾時
請求 - Ping
{
"type": "health",
"data": {
"action": "ping"
}
}
回應 - Pong
{
"type": "health",
"data": {
"action": "pong"
}
}
斷線續接(Session Resume)
WebSocket 連線若因網路波動意外中斷,可在**寬限期(grace period,預設 45 秒)**內帶著
resume_token 重新連線,接回原本的錄音會話——沿用同一個 task_id 與句子編號(sid),
逐字稿時間軸從斷點接續,無需重新開始整場錄音。
適用場景:行動網路切換、電梯/隧道短暫斷網、Wi-Fi 漫遊等。短暫網路抖動(TCP 可在 數秒內自動恢復者)連線本身不會中斷;斷線續接是針對「連線確實被切斷」的情形。
運作流程
1. start 成功 → session_started 內含 resume_token + resume_grace_seconds(請保存)
2. 連線中斷 → 在 grace 期間內:
a. 重新向後端取得一張「新的」一次性 Ticket
b. 以 Sec-WebSocket-Protocol: ["ticket.<新ticket>", "resume.<resume_token>"] 重連
3. 成功 → 伺服器回 resume_ok(含 server_last_sid)→ 像 start 後一樣「重開一段音訊流」
失敗 → 伺服器回 resume_* 錯誤 → 重新取 Ticket 後 fallback 全新 start(使用者已結束錄音時除外)
session_started 新增欄位
{
"type": "voice-translation",
"data": {
"action": "session_started",
"session_id": "...",
"task_id": "...",
"resume_token": "<43 字元,請保存到本次會話結束>",
"resume_grace_seconds": 45,
"server_time": 1749550000000
}
}
server_time為伺服器當下 unix 毫秒,前端可用「(server_time, 收到當下的 client 時間)」算出與伺服器的時鐘偏差作為參考;但 grace 寬限期倒數仍以 client 端 wall-clock 為準(斷線瞬間伺服器無法再推訊息,剩餘時間需前端自行從斷線時刻計算)。
重連握手
重連時 Sec-WebSocket-Protocol 同時帶 ticket 與 resume token(ticket 一律放前面):
const ws = new WebSocket(url, [`ticket.${newTicket}`, `resume.${resumeToken}`]);
- 每次重連都必須取一張新的 Ticket(Ticket 為一次性、用後即焚)。
- 伺服器先驗 Ticket(重新認證 + 配額),再以 Ticket 解出的
user_id/api_key_id比對resume_token的歸屬。
成功回應 - resume_ok
{
"type": "voice-translation",
"data": {
"action": "resume_ok",
"session_id": "...",
"task_id": "...",
"recording_type": "transcribe",
"recognition_mode": "single",
"server_last_sid": 42,
"server_last_offset_ms": 125000,
"server_recording_ms": 127500,
"is_paused": false,
"settings": {
"speaking_speed": "normal",
"profanity_handling": "mask",
"audio_format": "pcm",
"transcription_languages": ["zh-TW"],
"translation_languages": ["en-US"],
"realtime_translation": true,
"auto_summary": true,
"summary_plain_text": false,
"summary_template": "meeting",
"summary_language": "zh-TW",
"summary_mode": "builtin",
"name": "產品會議"
},
"message": "已續接原會話"
}
}
收到 resume_ok 後:
- 像
start之後一樣重新開始送音訊流。WebM/Opus 格式必須送一個「全新容器」 (重啟 encoder),不可接續舊容器;PCM 則直接續送即可。 server_last_sid為伺服器目前最後一個句子編號。若你本地收到的最大sid小於它, 代表斷線瞬間有少數句子在傳輸中遺失——即時畫面允許短暫缺號,完整內容最終會出現在 錄音結束的逐字稿(transcript)中。server_last_offset_ms是斷點對應的逐字稿時間軸位置(毫秒),基於已處理的音訊長度 (非真實經過時間;斷線期間音訊時間軸是凍結的)。前端可用它把重連後的新內容接在正確的 時間軸位置,與server_last_sid搭配對齊。server_recording_ms是續接後逐字稿時間軸實際接續的錄音頭時間(毫秒,含靜音)。前端用它把 錄音秒數標頭對齊到逐字稿所用的同一條時間軸;與server_last_offset_ms的差值即為斷點前 最後一句定稿之後、尚未產出逐字稿的尾端音訊(靜音或未斷句的語音)。省略時為 0。is_paused是續接後伺服器認定的暫停狀態。若為true,代表斷線前使用者已暫停——前端重開 音訊流後應立即暫停、不送音訊,並維持暫停 UI,不可逕自恢復錄音;false或省略則正常錄音。此欄位 讓「斷網重連」與「整頁 refresh 後續接」都能對齊暫停狀態(refresh 後本地暫停記憶已遺失,須以伺服器為準)。settings是伺服器權威的本次錄音當前設定(語速、翻譯語言、摘要設定、名稱等),供前端重連後 對帳、覆寫本地快取,詳見下方settings物件。
注意:兩種時間不可混用:逐字稿時間軸(
server_last_offset_ms)基於音檔、斷線時凍結; grace 重連窗口(resume_grace_seconds)是 wall-clock 真實時間、斷線時持續倒數。 判斷「還能不能重連」必須用 wall-clock,不能用音檔時間位置。
settings 物件(v1.6.4 新增)
resume_ok 會帶回 session 目前持有的本次錄音設定,供前端以伺服器權威值對帳(state reconcile),消除本地快取在暫停/恢復、斷線續接、多分頁等邊界情境下的漂移。
三個原則:
- 當前值:回傳錄音中經
set_speaking_speed/config/set_name/set_tts等操作變更後的結果,而非start當下的快照。 - API 格式:不做內部轉換(如
speaking_speed回"normal"等級字串,而非其對應的數值)。 recording_type/recognition_mode已在resume_ok頂層,不重複放入。
| 欄位 | 類型 | 說明 |
|---|---|---|
speaking_speed | string | 當前語速(very_slow / slow / normal / fast / very_fast;未設回傳 normal) |
profanity_handling | string | 敏感詞處理(mask / remove / show;未設回傳預設 mask) |
audio_format | string | start 的音訊格式(pcm / webm)。續接必須沿用同一格式(伺服器以原格式解碼,不重新協商) |
transcription_languages | string[] | 來源語言(互譯模式中途改語言後,請以 speaker_language_map 為準) |
translation_languages | string[] | 翻譯目標語言 |
realtime_translation | boolean | 即時翻譯開關(必帶,false 亦有意義) |
tts_enabled | boolean | TTS 開關(互譯模式或單人 TTS 啟用時帶) |
tts_mode | string | TTS 模式 sync / async(同上) |
tts_config | object | TTS 語音設定(互譯雙語 / 廣播多語 / 單人單語;{ "語言": { "voice", "speaking_rate" } }) |
conversation_mode | string | 互譯對話模式 auto / manual(可經 switch_conversation_mode 中途變更;僅互譯模式帶) |
speaker_language_map | object | 互譯 speaker 語言映射 { "1": "zh-TW", "2": "en-US" }(可經 set_speaker_language 中途變更;僅互譯模式帶,不論 start 時是否提供 speakers 都會帶) |
terminology | array | 術語庫(config 累積後的當前值;不含語言分組) |
fuzzy_correction | array | 模糊詞校正 [{ "correct", "incorrect": [], "case_insensitive" }](同上;case_insensitive 為 false 時省略) |
translation_dict | object | array | 翻譯字典。格式與你最後一次送出的相同——送語言分組格式({ "語言代碼": [{ "source", "target", "case_sensitive" }] })回語言分組格式,送舊的條目陣列格式回條目陣列格式。case_sensitive 為 false 時省略該欄位 |
auto_summary | boolean | 是否自動生成摘要(必帶,false 亦有意義) |
summary_plain_text | boolean | 摘要是否純文字輸出(必帶,false 亦有意義) |
summary_template | string | 摘要模板 slug |
summary_language | string | 摘要輸出語言 |
summary_mode | string | builtin / custom |
summary_prompt | string | mode-aware:custom=完整 prompt / builtin=補充指示(有設定才帶) |
summary_prompt_slug | string | custom 模式才有值 |
name | string | 當前錄音名稱(set_name 變更後的值) |
channel_mode | string | 多聲道(recognition_mode: "multi_channel")才帶:聲道模式(per_channel / shared) |
channels | array | 多聲道才帶:全部聲道的當前快照(含已移除的路),見下方說明 |
空集合欄位(如未設定術語庫)會整個省略;請以「欄位不存在=未設定」處理。
多聲道欄位 channel_mode / channels[](v1.10.0 新增)
多聲道錄音(recognition_mode: "multi_channel")的 settings 會額外帶回 channel_mode 與
channels[]。斷線續接不會重新驗證 start 參數——重連後前端唯一能確認「伺服器仍在多聲道模式、
聲道與語言的綁定沒有變」的依據就是這份快照,請以它覆寫本地的聲道狀態。shared 模式各路不綁語言,
全場語言以 settings.transcription_languages 為準。
channels[] 每路欄位:
| 欄位 | 類型 | 說明 |
|---|---|---|
channel_id | int | 聲道編號 |
speaker_name | string | 該路語者名稱(有設定才帶) |
transcription_languages | string[] | 該路綁定的語言(恰一個;set_channel_language 變更後的當前值)。shared 模式不帶 |
status | string | 該路狀態:preparing(準備中,尚未開始出字)/ready(已開始出字)/removed(已被 remove_channel 停用)/error(異常且無法自動恢復)。shared 模式其他聲道的 preparing/ready/error 跟著第一路 |
注意:已移除的路也會出現在快照中(
status: "removed"),不是遺漏:聲道編號不可重用 (含已移除的編號)。重連後請勿把removed的編號再拿去add_channel——伺服器會回channel_id_in_use。多聲道的完整說明請參考 語音翻譯服務。
失敗回應與處理
resume 失敗一律回 severity: "error"(非 fatal)。任一失敗 code 都代表本次握手所附的
Ticket 已被消費,fallback 前請先取一張新 Ticket 再送全新 start。使用者已結束錄音(已送出 stop)時,
請不要 fallback,以免開始一場使用者沒有要求的新錄音。
error_code | 意義 | 串接方處理 |
|---|---|---|
resume_token_invalid | token 無效或不存在;服務重新啟動或更新時,正在等待續接的錄音會直接收尾,之後續接也會收到這個錯誤 | 取新 Ticket → 全新 start |
resume_grace_expired | 寬限期已逾時,會話已結束 | 取新 Ticket → 全新 start |
resume_ownership_mismatch | token 歸屬不符(user/api_key) | 取新 Ticket → 全新 start |
resume_unavailable | 暫不可用(此連線無法續接原場次;原場次已送出 stop 或已被結束、仍在處理中時也會收到) | 取新 Ticket → 全新 start |
服務重新啟動或更新時,正在等待續接的錄音會直接收尾:錄音照常保存並送出完成通知。錄音中、沒有斷線的連線不受影響,會先收到
service_shutdown,可以把這一場錄完。服務關閉期間送出的start會收到service_shutdown,錄音不會開始,之後連線會關閉;請稍後重新連線再開始錄音。
斷線期間的內容
- 斷線那幾秒的音訊不會補回(伺服器以「暫停」語義處理):逐字稿時間軸無縫接續,但 斷線時段不會出現在錄音中,斷線期間也不另外計費。斷線當下那一分鐘已在開始時扣點,不會退回; 斷線期間到了下一分鐘時,會在續接成功後才扣。
- 互譯模式:斷線當下「已說出但尚未斷句」的半句,會以暫定結果補一句。
- 單人 / 語者分離模式:斷線當下未斷句的那一句不保證保留。
信任模型與安全
api_key即信任邊界:伺服器採「最後連線者勝(takeover)」策略。任何持有同一把api_key且取得對應resume_token的客戶端,皆可在寬限期內接管同一會話(原連線會被 靜默斷開)。因此請勿跨信任域共用同一把api_key;B2B 團隊共用金鑰視為單一信任邊界。- 僅可於 WSS(TLS)傳遞:
resume_token與Ticket均透過Sec-WebSocket-Protocol傳送,任何明文(非 TLS)入口都視為憑證洩漏。
重連實作範例(JavaScript)
以下封裝了完整的斷線續接邏輯:保存 token、偵測斷線、寬限期內重連、resume_ok / 失敗分流。
音訊擷取(startAudioStream 等)依你的格式(PCM / WebM)自行實作;WebM 於 resume_ok 後必須以全新容器重開。
class ResumableVASClient {
// getTicket: async () => string(每次都向後端取「新的」一次性 Ticket)
constructor({ wsUrl, getTicket, buildStartMessage }) {
this.wsUrl = wsUrl;
this.getTicket = getTicket;
this.buildStartMessage = buildStartMessage; // () => start 訊息物件
this.resumeToken = null;
this.graceSeconds = 45;
this.resumeDeadline = 0; // 寬限期截止(epoch ms);0 = 未在續接中
this.closedByUser = false;
}
async start() {
this.closedByUser = false;
await this._connect(false);
}
stop() {
this.closedByUser = true;
this.resumeToken = null;
this.ws?.close(1000, 'client stop');
}
async _connect(isResume) {
const ticket = await this.getTicket(); // 一次性,每次重連都要新的
const protocols = isResume && this.resumeToken
? [`ticket.${ticket}`, `resume.${this.resumeToken}`]
: [`ticket.${ticket}`];
this.ws = new WebSocket(this.wsUrl, protocols);
this.ws.onopen = () => {
if (!isResume) this.ws.send(JSON.stringify(this.buildStartMessage())); // 全新 start
// isResume:等伺服器回 resume_ok 再重開音訊流(見下方 onmessage)
};
// staleness guard:舊 socket 遲到的事件(e.target !== this.ws)一律忽略,避免 resume
// fallback 後雙連線、或舊連線的 onclose 污染新狀態。
this.ws.onmessage = (e) => { if (e.target === this.ws) this._onMessage(JSON.parse(e.data)); };
this.ws.onclose = (e) => { if (e.target === this.ws) this._onClose(); };
this.ws.onerror = () => {}; // onclose 會統一處理
}
_onMessage(msg) {
const d = msg.data || {};
switch (d.action) {
case 'session_started':
this.resumeToken = d.resume_token; // 保存 token
this.graceSeconds = d.resume_grace_seconds || 45;
this.resumeDeadline = 0; // 連線正常,清掉續接狀態
this.startAudioStream(); // 開始送音訊
break;
case 'resume_ok':
// 續接成功:像 start 後一樣重開音訊流(WebM 必送新容器)
this.resumeDeadline = 0;
this.restartAudioStream();
break;
default:
if (msg.type === 'error' && String(d.error_code).startsWith('resume_')) {
// 續接失敗 → 清 token、fallback 全新 start(_connect 會取新 ticket)
this.resumeToken = null;
this.resumeDeadline = 0;
// 使用者已結束錄音(例如續接途中呼叫了 stop()):不要再開始新的錄音
if (this.closedByUser) return;
this._connect(false).catch((err) => this.onFatal(err));
}
// 其餘事件(origin / translation 等)依 sid 更新 UI
}
}
_onClose() {
if (this.closedByUser || !this.resumeToken) return this.onFatal?.();
// 第一次斷線:設定寬限期截止
if (this.resumeDeadline === 0) {
this.resumeDeadline = Date.now() + this.graceSeconds * 1000;
}
if (Date.now() < this.resumeDeadline) {
// 寬限期內:嘗試續接(失敗則退避重試,仍在期限內)
this._connect(true).catch(() => {
if (Date.now() < this.resumeDeadline) setTimeout(() => this._onClose(), 1000);
else this.onResumeExhausted?.();
});
} else {
this.onResumeExhausted?.(); // 寬限期已過,放棄續接
}
}
// 以下由你依音訊格式實作:
startAudioStream() {} // 開始擷取並送 audio frame
restartAudioStream() {} // 重開音訊流(WebM=新容器;PCM=續送)
onResumeExhausted() {} // 寬限期內未能續接(視為本場結束 / 提示使用者)
onFatal() {} // 不可恢復
}
要點:(1) 每次重連都取新 Ticket;(2)
resume_ok後才重開音訊流;(3)resume_*錯誤時 fallback 全新start,但使用者已結束錄音時不要 fallback;(4) WebM 重開須送新容器。
版本:V1.24.1 最後更新:2026-10-07