协议: WSS (WebSocket Secure)
地址: wss://{domain}/ws
interface ConnectionParams {
userId?: string; // 用户ID(可选)
sessionId?: string; // 会话ID(可选,用于恢复会话)
}
import ReconnectingWebSocket from 'reconnecting-websocket';
const ws = new ReconnectingWebSocket('wss://example.com/ws');
ws.onopen = () => {
console.log('WebSocket已连接');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
ws.onclose = () => {
console.log('WebSocket已断开');
};
interface BaseMessage {
type: string; // 消息类型
requestId: string; // 请求唯一标识(UUID)
timestamp: number; // 时间戳(毫秒)
}
audio_chunk - 音频数据块audio_end - 音频结束cancel - 取消请求text_result - 文本识别结果emotion - 情感标签audio_response - 音频响应error - 错误消息ping / pong - 心跳interface AudioChunkMessage extends BaseMessage {
type: 'audio_chunk';
requestId: string; // 本次录音的唯一ID
data: string; // Base64编码的PCM音频数据
sampleRate: 16000; // 采样率
channels: 1; // 声道数
bitDepth: 16; // 采样位数
chunkIndex: number; // 分块序号(从0开始)
}
音频规格:
发送示例:
const message = {
type: 'audio_chunk',
requestId: 'uuid-xxx',
data: btoa(String.fromCharCode(...pcmData)),
sampleRate: 16000,
channels: 1,
bitDepth: 16,
chunkIndex: 0,
timestamp: Date.now()
};
ws.send(JSON.stringify(message));
interface AudioEndMessage extends BaseMessage {
type: 'audio_end';
requestId: string; // 对应的录音ID
totalChunks: number; // 总分块数
}
发送时机: 用户点击【发送】按钮时
interface CancelMessage extends BaseMessage {
type: 'cancel';
requestId: string; // 要取消的录音ID
}
发送时机: 用户点击【取消】按钮时
处理逻辑:
interface PingMessage extends BaseMessage {
type: 'ping';
requestId: string;
}
发送频率: 每30秒
interface TextResultMessage extends BaseMessage {
type: 'text_result';
requestId: string; // 对应的录音ID
text: string; // 识别的文本
isFinal: boolean; // 是否为最终结果
}
说明:
isFinal: true 表示最终识别结果interface EmotionMessage extends BaseMessage {
type: 'emotion';
requestId: string; // 对应的录音ID
emotion: EmotionType; // 情感类型
intensity?: number; // 情感强度(0-1)
}
type EmotionType =
| 'neutral' // 中性
| 'happy' // 开心
| 'sad' // 悲伤
| 'excited' // 兴奋
| 'angry' // 生气
| 'surprised' // 惊讶
| 'thoughtful' // 思考
| 'encouraging'; // 鼓励
用途: 驱动前端动画IP表情变化
interface AudioResponseMessage extends BaseMessage {
type: 'audio_response';
requestId: string; // 对应的录音ID
data: string; // Base64编码的PCM音频数据
sampleRate: 16000; // 采样率
channels: 1; // 声道数
bitDepth: 16; // 采样位数
chunkIndex: number; // 分块序号
isLast: boolean; // 是否为最后一块
}
说明:
isLast: true 表示最后一块音频接收示例:
const audioQueue = [];
function handleAudioResponse(message) {
if (message.type === 'audio_response') {
const pcmData = atob(message.data);
const audioBuffer = new Uint8Array(pcmData.length);
for (let i = 0; i < pcmData.length; i++) {
audioBuffer[i] = pcmData.charCodeAt(i);
}
audioQueue.push({
buffer: audioBuffer,
isLast: message.isLast
});
playNextAudio();
}
}
interface ErrorMessage extends BaseMessage {
type: 'error';
requestId: string; // 对应的请求ID
code: string; // 错误代码
message: string; // 错误描述
}
错误代码:
STT_ERROR - 语音识别失败LLM_ERROR - 文本生成失败TTS_ERROR - 语音合成失败NETWORK_ERROR - 网络错误RATE_LIMIT - API限流INVALID_AUDIO - 音频格式错误CANCELLED - 请求已取消interface PongMessage extends BaseMessage {
type: 'pong';
requestId: string;
}
客户端 服务端
| |
|----(1) audio_chunk (chunkIndex=0)-->|
|----(2) audio_chunk (chunkIndex=1)-->|
|----(3) audio_chunk (chunkIndex=2)-->| --> STT处理
| |
|<---(4) text_result (isFinal=false)--|
| |
|----(5) audio_end------------------->| --> LLM处理
| |
|<---(6) text_result (isFinal=true)---|
|<---(7) emotion----------------------|
| | --> TTS处理
|<---(8) audio_response (chunk 0)-----|
|<---(9) audio_response (chunk 1)-----|
|<---(10) audio_response (chunk 2, isLast)--|
| |
客户端 服务端
| |
|----(1) audio_chunk (chunkIndex=0)-->|
|----(2) audio_chunk (chunkIndex=1)-->| --> STT处理中
| |
|----(3) cancel---------------------->| --> 停止处理
| | 丢弃结果
| |
客户端 服务端
| |
|----(1) audio_chunk------------------>|
|----(2) audio_end-------------------->| --> STT失败
| |
|<---(3) error (code: STT_ERROR)------|
| |
type ClientState =
| 'idle' // 空闲(等待状态)
| 'recording' // 录音中(听取状态)
| 'sending' // 发送中(等待响应)
| 'playing' // 播放中(说话状态)
| 'error'; // 错误状态
interface ClientStateManager {
currentState: ClientState;
currentRequestId: string | null;
audioQueue: AudioBuffer[];
startRecording(): void;
stopRecording(): void;
sendAudio(): void;
cancelRequest(): void;
playAudioResponse(): void;
}
idle -> recording: 用户按下录音按钮
recording -> idle: 用户点击取消
recording -> sending: 用户松开按钮/点击发送
sending -> playing: 收到首个audio_response
playing -> idle: 音频播放完成
任意状态 -> error: 发生错误
error -> idle: 错误恢复
class AudioPlayer {
private minBufferSize = 2; // 最小缓冲块数
private audioQueue: AudioBuffer[] = [];
private isPlaying = false;
addChunk(audioBuffer: AudioBuffer) {
this.audioQueue.push(audioBuffer);
// 达到最小缓冲即开始播放
if (!this.isPlaying && this.audioQueue.length >= this.minBufferSize) {
this.play();
}
}
play() {
// 播放逻辑
}
}
class WebSocketManager {
private pingInterval = 30000; // 30秒
private pongTimeout = 5000; // 5秒超时
startHeartbeat() {
setInterval(() => {
this.ws.send(JSON.stringify({
type: 'ping',
requestId: generateUUID(),
timestamp: Date.now()
}));
// 等待pong响应
setTimeout(() => {
if (!this.receivedPong) {
// 未收到pong,重连
this.reconnect();
}
}, this.pongTimeout);
}, this.pingInterval);
}
}
const ws = new ReconnectingWebSocket('wss://example.com/ws', [], {
maxRetries: 3,
connectionTimeout: 5000,
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.5
});
# 服务端验证
def validate_audio_chunk(message: dict) -> bool:
required_fields = ['type', 'requestId', 'data', 'sampleRate',
'channels', 'bitDepth', 'chunkIndex']
if not all(field in message for field in required_fields):
return False
if message['sampleRate'] != 16000:
return False
if message['bitDepth'] != 16:
return False
return True
function handleError(error: ErrorMessage) {
switch (error.code) {
case 'STT_ERROR':
showToast('语音识别失败,请重试');
break;
case 'LLM_ERROR':
showToast('生成回复失败,请重试');
break;
case 'TTS_ERROR':
showToast('语音合成失败,请重试');
break;
case 'RATE_LIMIT':
showToast('请求过于频繁,请稍后再试');
break;
default:
showToast('发生错误,请重试');
}
// 重置状态
resetToIdle();
}
interface LogEntry {
timestamp: number;
requestId: string;
type: 'send' | 'receive' | 'error';
messageType: string;
data?: any;
duration?: number; // 耗时(毫秒)
}
class PerformanceMonitor {
logLatency(requestId: string, stage: string, duration: number) {
console.log(`[${requestId}] ${stage}: ${duration}ms`);
}
trackFullLatency(requestId: string) {
const start = Date.now();
return {
markSTT: () => this.logLatency(requestId, 'STT', Date.now() - start),
markLLM: () => this.logLatency(requestId, 'LLM', Date.now() - start),
markTTS: () => this.logLatency(requestId, 'TTS', Date.now() - start),
markTotal: () => this.logLatency(requestId, 'Total', Date.now() - start)
};
}
}
// 消息类型
type MessageType =
| 'audio_chunk'
| 'audio_end'
| 'cancel'
| 'text_result'
| 'emotion'
| 'audio_response'
| 'error'
| 'ping'
| 'pong';
// 情感类型
type EmotionType =
| 'neutral'
| 'happy'
| 'sad'
| 'excited'
| 'angry'
| 'surprised'
| 'thoughtful'
| 'encouraging';
// 错误代码
type ErrorCode =
| 'STT_ERROR'
| 'LLM_ERROR'
| 'TTS_ERROR'
| 'NETWORK_ERROR'
| 'RATE_LIMIT'
| 'INVALID_AUDIO'
| 'CANCELLED';
// 所有消息类型的联合
type Message =
| AudioChunkMessage
| AudioEndMessage
| CancelMessage
| TextResultMessage
| EmotionMessage
| AudioResponseMessage
| ErrorMessage
| PingMessage
| PongMessage;
参考: 开源组件推荐清单.md - Recorder.js + reconnecting-websocket
参考: 开源组件推荐清单.md - FastAPI + DashScope SDK
sk-1c11338d4c3b44bca4ab407c558a67cchttps://dashscope.aliyuncs.comparaformer-realtime-v2qwen-plusqwen3-tts-flash