| 本文档面向设备端/客户端开发,说明如何与服务端 ChatBot 进行鉴权、建立 WebSocket 连接、上传语音音频并接收 TTS 音频与转写文本。
|
|
|
| 1. 术语与前置条件
|
|
|
| - device_id:设备唯一标识(服务端已注册)。
|
| - device_secret:设备密钥,仅在注册时返回一次,需安全存储在设备端,不可明文上报。
|
| - nonce:服务端下发的随机挑战串,缓存 60s,仅能使用一次。
|
| - signature/sign:HMAC_SHA256(device_secret, nonce) 的十六进制小写字符串。
|
| - access token:JWT,包含 device_id 等 claim,用于 WS 鉴权。
|
|
|
| 部署建议:
|
|
|
| - 生产环境必须使用 HTTPS/WSS。
|
| - 服务端依赖缓存(通常是 Redis)存储 nonce 与用量统计。
|
|
|
| 2. 鉴权流程(必须)
|
|
|
| ChatBot WebSocket 连接需要同时提供:
|
|
|
| - token:JWT access token
|
| - nonce:一次性挑战串
|
| - sign:对该 nonce 的 HMAC 签名
|
|
|
| 注意:/device/token/ 在签发 token 成功后会删除该次 challenge 的 nonce(防重放)。因此建议按如下顺序做两次 challenge:
|
|
|
| 1. 获取 token 用的 challenge
|
| 2. 获取 WS 连接用的 challenge
|
|
|
| 2.1 获取 access/refresh token(Challenge-Response)
|
|
|
| 两步 challenge-response(避免明文传输 device_secret):
|
|
|
| 1. 请求 nonce(challenge)
|
|
|
| POST /device/challenge/
|
|
|
| 请求:
|
| { "device_id": "LYY-202603-000123" }
|
|
|
| 响应:
|
| { "nonce": "..." }
|
|
|
| 2. 计算签名并换取 token
|
|
|
| - 在设备端计算 signature = HMAC_SHA256(device_secret, nonce) 并输出 hex(device_secret 不上送)。
|
|
|
| 在设备端计算:
|
|
|
| signature = hex(HMAC_SHA256(key=device_secret, msg=nonce))
|
|
|
| POST /device/token/
|
|
|
| 请求:
|
| { "device_id": "LYY-202603-000123", "signature": "<hex-hmac-sha256>" }
|
|
|
| 响应:
|
| { "access": "<access_jwt>", "refresh": "<refresh_jwt>" }
|
|
|
| 补充:
|
|
|
| - nonce 在服务端缓存时间很短(约 60s),且成功使用后会被删除;过期或已使用需要重新获取 nonce。
|
|
|
| 2.2 刷新 access token(SimpleJWT 标准接口)
|
|
|
| access token 过期后,使用 refresh token 换取新的 access:
|
|
|
| POST /api/token/refresh/
|
|
|
| 请求:
|
| { "refresh": "<refresh_token>" }
|
|
|
| 响应:
|
| { "access": "<new_access_token>" }
|
|
|
| 2.3 获取 WS 连接用的 nonce/sign(每次连接前都做一次)
|
|
|
| 在每次发起 WebSocket 连接之前,再调用一次:
|
|
|
| POST /device/challenge/ -> 得到 nonce_ws
|
|
|
| 在设备端计算:
|
|
|
| sign_ws = hex(HMAC_SHA256(key=device_secret, msg=nonce_ws))
|
|
|
| 随后用 access token + nonce_ws + sign_ws 建立 WS 连接(见下一节)。
|
|
|
| 3. WebSocket 连接信息
|
|
|
| 3.1 连接地址
|
|
|
| 路径(服务端路由固定):
|
|
|
| - GET /ws/chatbot/
|
|
|
| 连接 URL(示例):
|
|
|
| wss://<host>/ws/chatbot/?token=<access>&nonce=<nonce_ws>&sign=<sign_ws>
|
|
|
| 3.2 语言选项参数(lang)
|
|
|
| 语言选项通过 Query String 附加在 WebSocket URL 末尾,在建立连接时一次性传入,连接期间不可更改(需重连才能切换语言)。
|
|
|
| 参数说明
|
|
|
| 参数
|
| 类型
|
| 说明
|
| lang
|
| string
|
| 指定 ASR 识别语言,不传则服务端默认偏中文识别
|
|
|
| 支持的语言值
|
|
|
| 显示名称
|
| URL 参数
|
| 说明
|
| 默认
|
| (不传 lang)
|
| 中英混合,偏中文
|
| 中文
|
| lang=zh
|
| 中英混合,偏中文
|
| 英语
|
| lang=en
|
| 中英混合,偏英文
|
| 广东话
|
| lang=zh_yue
|
| 粤语方言
|
| 四川话
|
| lang=zh_sc
|
| 四川方言
|
| 苏州话
|
| lang=zh_su
|
| 苏州方言
|
| 日语
|
| lang=ja
|
|
|
| 西班牙语
|
| lang=es
|
|
|
| 俄语
|
| lang=ru
|
|
|
| 韩语
|
| lang=ko
|
|
|
| 越南语
|
| lang=vi
|
|
|
| 德语
|
| lang=de
|
|
|
| 阿拉伯语
|
| lang=ar
|
|
|
| 印尼语
|
| lang=id
|
|
|
| 泰语
|
| lang=th
|
|
|
| 马来语
|
| lang=ms
|
|
|
| 葡萄牙语
|
| lang=pt
|
|
|
| 乌兹别克语
|
| lang=uz
|
|
|
| 波兰语
|
| lang=pl
|
|
|
| 波斯语
|
| lang=fa
|
|
|
|
|
| 连接 URL 示例
|
|
|
| 不传语言参数(默认中英混合):
|
| wss://<host>/ws/chatbot/?token=<access>&nonce=<nonce_ws>&sign=<sign_ws>
|
|
|
| 指定中文(lang=zh):
|
| wss://<host>/ws/chatbot/?token=<access>&nonce=<nonce_ws>&sign=<sign_ws>&lang=zh
|
|
|
| 指定日语(lang=ja):
|
| wss://<host>/ws/chatbot/?token=<access>&nonce=<nonce_ws>&sign=<sign_ws>&lang=ja
|
|
|
| 代码示例(Python)
|
|
|
| from urllib.parse import urlencode
|
|
|
| # 基础参数
|
| params = {
|
| "token": access_token,
|
| "nonce": nonce_ws,
|
| "sign": sign_ws,
|
| }
|
|
|
| # lang 示例值:
|
| # "" -> 不传 lang(默认,中英混合偏中文)
|
| # "zh" -> 中文
|
| # "en" -> 英语(中英混合偏英文)
|
| # "ja" -> 日语
|
| # "zh_yue" -> 广东话
|
| lang = "zh" # 按需修改
|
|
|
| if lang:
|
| params["lang"] = lang
|
|
|
| ws_url = f"wss://<host>/ws/chatbot/?{urlencode(params)}"
|
|
|
| 注意:语言参数在认证(获取 nonce_ws/sign_ws)之后、建立 WebSocket 连接之前拼入 URL。切换语言时需重新走一次认证流程并重建连接。
|
|
|
| 3.2 关闭码(Close Code)
|
|
|
| 服务端会在握手后主动关闭(客户端应根据 code 做重试或提示):
|
|
|
| - 4001:未授权(token 无效 / nonce 不存在或过期 / sign 校验失败 / 设备不存在或被禁用)
|
| - 4002:当日使用时长超限(见 6. 用量限制)
|
|
|
| 4. 消息协议(客户端与服务端之间)
|
|
|
| 该 WS 同时使用两种 frame:
|
|
|
| - Binary frame:原始音频字节流
|
| - Text frame:JSON 控制/文本消息
|
|
|
| 4.1 上行(客户端 -> 服务端)
|
|
|
| 1. 语音输入(binary)
|
|
|
| - 客户端在“按住说话”期间,持续发送 binary frame。
|
| - binary 内容为原始 PCM 音频字节(服务端声明为 pcm16 输入)。
|
| - 建议:小块发送(例如 20ms 到 100ms 一帧),避免单帧过大造成延迟抖动。
|
|
|
| 2. 松开按键/停止说话(text)
|
|
|
| 当用户结束一轮说话(例如松开按钮)后,发送:
|
| {"action":"stop_speaking"}
|
|
|
| 服务端会提交输入缓冲并触发一次回答生成。
|
|
|
| 3. 打断当前回答(text)
|
|
|
| 当用户需要打断正在播报的回答并立即开始新一轮说话时,发送:
|
| {"action":"interrupt"}
|
|
|
| 服务端会取消当前回答并清空输入缓冲,随后你可以继续发送新的音频 binary frame。
|
|
|
| 4.2 下行(服务端 -> 客户端)
|
|
|
| 1. TTS 音频输出(binary)
|
|
|
| - 服务端会以 binary frame 方式持续下发回答音频 chunk,客户端应边收边播放。
|
| - 服务端当前配置的输出音频格式为 pcm24(24-bit PCM 原始字节流)。
|
|
|
| 2. 文本转写增量(text)
|
|
|
| 服务端会下发 text frame(JSON)表示增量转写:
|
| {"action":"transcript","text":"..."}
|
|
|
| 说明:
|
|
|
| - text 为增量片段(delta),客户端可将其追加到当前显示文本上。
|
|
|
| 5. 推荐的客户端状态机
|
|
|
| 1. 连接前:确保已有 access token,并获取 nonce_ws/sign_ws。
|
| 2. 建立 WS:连接成功后进入 IDLE。
|
| 3. IDLE -> RECORDING:用户按住说话,开始周期性发送 PCM16 binary 帧。
|
| 4. RECORDING -> WAITING:用户松开按钮,发送 {"action":"stop_speaking"},停止上行音频。
|
| 5. WAITING -> PLAYING:开始接收回答音频 binary 并播放,同时接收转写 text 并显示。
|
| 6. PLAYING -> RECORDING:若用户打断,先发送 {"action":"interrupt"},停止播放,立刻进入录音并发送新音频。
|
| 7. 任意状态断线:若 close code 为 4001/4002 按原因处理;否则重新获取 nonce_ws/sign_ws 后重连。
|
|
|
| 6. 用量限制与连接管理
|
|
|
| 服务端会按“连接持续时间”累计当日使用时长(不是纯说话时长):
|
|
|
| - 当日上限:5 小时(5 * 60 * 60 秒)
|
| - 超限后将拒绝新连接(close code 4002)
|
|
|
| 建议:
|
|
|
| - 不用时及时断开 WS,避免空闲连接持续计费。
|
| - 断线重连前重新获取 nonce_ws/sign_ws,不要复用旧 nonce。
|
|
|
| 7. 示例代码(Python,演示握手与 WS 交互骨架)
|
|
|
| 下面示例只演示流程与消息格式;音频采集与播放请按你的平台实现。
|
|
|
| import requests, hmac, hashlib, json, asyncio, websockets
|
|
|
| HOST = ""
|
| BASE_HTTP = f"https://{HOST}"
|
| BASE_WS = f"wss://{HOST}"
|
|
|
| DEVICE_ID = "LYY-202603-000123"
|
| DEVICE_SECRET = "<device_secret>"
|
|
|
| def hmac_hex(secret: str, nonce: str) -> str:
|
| return hmac.new(secret.encode("utf-8"), nonce.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
|
| def get_nonce() -> str:
|
| r = requests.post(f"{BASE_HTTP}/dev
|