WebSocket API

WebSocket 連線與認證

目錄

  1. 連線資訊
  2. 認證方式
  3. 訊息格式
  4. 心跳機制(Health)

連線資訊

項目值
端點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
}
欄位類型說明
ticketstring一次性 Ticket(32 字元)
expires_inint有效期(秒)

步驟 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_invalid401Ticket 無效或已過期
ticket_expired401Ticket 已過期
ticket_already_used401Ticket 已被使用
ticket_validation_failed500Ticket 驗證失敗

完整 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_codestring錯誤碼(程式化處理用)
severitystring嚴重程度:fatal / error / warning
messagestring人類可讀的錯誤訊息
contextstring錯誤來源分類
request_idstring請求追蹤 ID
timestampstring錯誤發生時間(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 後:

  1. 像 start 之後一樣重新開始送音訊流。WebM/Opus 格式必須送一個「全新容器」 (重啟 encoder),不可接續舊容器;PCM 則直接續送即可。
  2. server_last_sid 為伺服器目前最後一個句子編號。若你本地收到的最大 sid 小於它, 代表斷線瞬間有少數句子在傳輸中遺失——即時畫面允許短暫缺號,完整內容最終會出現在 錄音結束的逐字稿(transcript)中。
  3. server_last_offset_ms 是斷點對應的逐字稿時間軸位置(毫秒),基於已處理的音訊長度 (非真實經過時間;斷線期間音訊時間軸是凍結的)。前端可用它把重連後的新內容接在正確的 時間軸位置,與 server_last_sid 搭配對齊。
  4. server_recording_ms 是續接後逐字稿時間軸實際接續的錄音頭時間(毫秒,含靜音)。前端用它把 錄音秒數標頭對齊到逐字稿所用的同一條時間軸;與 server_last_offset_ms 的差值即為斷點前 最後一句定稿之後、尚未產出逐字稿的尾端音訊(靜音或未斷句的語音)。省略時為 0。
  5. is_paused 是續接後伺服器認定的暫停狀態。若為 true,代表斷線前使用者已暫停——前端重開 音訊流後應立即暫停、不送音訊,並維持暫停 UI,不可逕自恢復錄音;false 或省略則正常錄音。此欄位 讓「斷網重連」與「整頁 refresh 後續接」都能對齊暫停狀態(refresh 後本地暫停記憶已遺失,須以伺服器為準)。
  6. 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_speedstring當前語速(very_slow / slow / normal / fast / very_fast;未設回傳 normal)
profanity_handlingstring敏感詞處理(mask / remove / show;未設回傳預設 mask)
audio_formatstringstart 的音訊格式(pcm / webm)。續接必須沿用同一格式(伺服器以原格式解碼,不重新協商)
transcription_languagesstring[]來源語言(互譯模式中途改語言後,請以 speaker_language_map 為準)
translation_languagesstring[]翻譯目標語言
realtime_translationboolean即時翻譯開關(必帶,false 亦有意義)
tts_enabledbooleanTTS 開關(互譯模式或單人 TTS 啟用時帶)
tts_modestringTTS 模式 sync / async(同上)
tts_configobjectTTS 語音設定(互譯雙語 / 廣播多語 / 單人單語;{ "語言": { "voice", "speaking_rate" } })
conversation_modestring互譯對話模式 auto / manual(可經 switch_conversation_mode 中途變更;僅互譯模式帶)
speaker_language_mapobject互譯 speaker 語言映射 { "1": "zh-TW", "2": "en-US" }(可經 set_speaker_language 中途變更;僅互譯模式帶,不論 start 時是否提供 speakers 都會帶)
terminologyarray術語庫(config 累積後的當前值;不含語言分組)
fuzzy_correctionarray模糊詞校正 [{ "correct", "incorrect": [], "case_insensitive" }](同上;case_insensitive 為 false 時省略)
translation_dictobject | array翻譯字典。格式與你最後一次送出的相同——送語言分組格式({ "語言代碼": [{ "source", "target", "case_sensitive" }] })回語言分組格式,送舊的條目陣列格式回條目陣列格式。case_sensitive 為 false 時省略該欄位
auto_summaryboolean是否自動生成摘要(必帶,false 亦有意義)
summary_plain_textboolean摘要是否純文字輸出(必帶,false 亦有意義)
summary_templatestring摘要模板 slug
summary_languagestring摘要輸出語言
summary_modestringbuiltin / custom
summary_promptstringmode-aware:custom=完整 prompt / builtin=補充指示(有設定才帶)
summary_prompt_slugstringcustom 模式才有值
namestring當前錄音名稱(set_name 變更後的值)
channel_modestring多聲道(recognition_mode: "multi_channel")才帶:聲道模式(per_channel / shared)
channelsarray多聲道才帶:全部聲道的當前快照(含已移除的路),見下方說明

空集合欄位(如未設定術語庫)會整個省略;請以「欄位不存在=未設定」處理。

多聲道欄位 channel_mode / channels[](v1.10.0 新增)

多聲道錄音(recognition_mode: "multi_channel")的 settings 會額外帶回 channel_mode 與 channels[]。斷線續接不會重新驗證 start 參數——重連後前端唯一能確認「伺服器仍在多聲道模式、 聲道與語言的綁定沒有變」的依據就是這份快照,請以它覆寫本地的聲道狀態。shared 模式各路不綁語言, 全場語言以 settings.transcription_languages 為準。

channels[] 每路欄位:

欄位類型說明
channel_idint聲道編號
speaker_namestring該路語者名稱(有設定才帶)
transcription_languagesstring[]該路綁定的語言(恰一個;set_channel_language 變更後的當前值)。shared 模式不帶
statusstring該路狀態: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_invalidtoken 無效或不存在;服務重新啟動或更新時,正在等待續接的錄音會直接收尾,之後續接也會收到這個錯誤取新 Ticket → 全新 start
resume_grace_expired寬限期已逾時,會話已結束取新 Ticket → 全新 start
resume_ownership_mismatchtoken 歸屬不符(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

Copyright © 2026