自由对话模块五阶段接口设计
背景
自由对话采用一次性 Session:
- 每次点击“开始自由对话”创建新的
sessionId
- 不保存长期 Conversation 档案
- 字幕、临时消息和实时事件到期自动删除
- 仅保留最小用量记录,用于额度、计费和厂商账单对齐
- 当前方案不绑定 Pion
- 客户端通过 WebRTC + DataChannel 直连 Qwen Realtime
- 业务后端负责鉴权、额度、Session、SDP 代理、状态、用量和数据清理
目标
为以下五个阶段定义统一接口:
- 开始
- 暂停
- 恢复
- 打断
- 结束
涉及四个端:
- UI
- Free Chat SDK
- 业务后端
- Qwen Realtime
一、开始
UI → SDK
await freeChatClient.start({
coachId: "clara",
voiceId: "clara_default",
difficulty: "BEGINNER",
language: "en-US"
});
SDK → 后端:创建 Session
POST /v1/free-chat/sessions
请求:
{
"coach_id": "clara",
"voice_id": "clara_default",
"difficulty": "BEGINNER",
"language": "en-US",
"client": {
"platform": "WEB",
"app_version": "0.1.0",
"device_id": "device_xxx"
},
"idempotency_key": "start_xxx"
}
响应:
{
"session_id": "fcs_xxx",
"status": "CREATED",
"expires_at": "2026-07-17T10:30:00Z",
"content_delete_at": "2026-07-18T10:30:00Z",
"policy": {
"max_duration_seconds": 600,
"remaining_seconds": 600,
"idle_timeout_seconds": 60
},
"realtime_config": {
"voice": "clara_default",
"modalities": ["text", "audio"],
"turn_detection": {
"type": "server_vad",
"threshold": 0.5,
"silence_duration_ms": 800
}
}
}
SDK → 后端:交换 SDP
POST /v1/free-chat/sessions/{sessionId}/sdp
Content-Type: application/sdp
发送:Offer SDP
接收:Answer SDP
SDK → Qwen
收到:
{
"type": "session.created",
"session": {
"id": "provider_session_xxx"
}
}
发送:
{
"type": "session.update",
"session": {
"instructions": "<server-generated-prompt>",
"voice": "clara_default",
"modalities": ["text", "audio"],
"turn_detection": {
"type": "server_vad",
"threshold": 0.5,
"prefix_padding_ms": 500,
"silence_duration_ms": 800
}
}
}
AI 主动开场:
{
"type": "response.create"
}
SDK → 后端:绑定厂商 Session
POST /v1/free-chat/sessions/{sessionId}/provider-session
{
"provider_session_id": "provider_session_xxx"
}
二、暂停
UI → SDK
await freeChatClient.pause();
SDK 内部
- AI 正在回复时发送
response.cancel
- 停止当前 AI 音频
- 清空未播放音频缓冲
- 禁用麦克风 Track
- 保持 PeerConnection 和 DataChannel 不关闭
SDK → 后端
POST /v1/free-chat/sessions/{sessionId}/pause
请求:
{
"reason": "USER_PAUSED",
"client_paused_at": "2026-07-17T10:23:00Z"
}
响应:
{
"session_id": "fcs_xxx",
"status": "PAUSED",
"remaining_seconds": 420,
"max_pause_seconds": 120
}
三、恢复
UI → SDK
await freeChatClient.resume();
SDK → 后端
POST /v1/free-chat/sessions/{sessionId}/resume
请求:
{
"client_resumed_at": "2026-07-17T10:23:20Z"
}
响应:
{
"session_id": "fcs_xxx",
"status": "ACTIVE",
"pause_duration_seconds": 20,
"remaining_seconds": 400
}
SDK 内部
- 后端确认恢复成功后再启用音频
- 恢复 AI 播放器
- 重新启用麦克风 Track
- 恢复后等待用户继续说话
- 默认不自动发送
response.create
四、打断
UI / VAD → SDK
await freeChatClient.interrupt("USER_SPEAKING");
SDK → Qwen
{
"event_id": "evt_interrupt_xxx",
"type": "response.cancel"
}
SDK 内部
- 停止当前 AI 音频
- 清空当前回复音频缓冲
- 保持麦克风开启
- 保持 WebRTC 和 DataChannel 连接
- 发出
response.interrupted
SDK → 后端(可选)
POST /v1/free-chat/sessions/{sessionId}/events
{
"event_id": "evt_interrupt_xxx",
"event_type": "ASSISTANT_RESPONSE_INTERRUPTED",
"occurred_at": "2026-07-17T10:24:10Z",
"payload": {
"reason": "USER_SPEAKING",
"provider_response_id": "resp_xxx"
}
}
打断事件上报失败不得影响实时打断。
五、结束
UI → SDK
await freeChatClient.end("USER_ENDED");
SDK 内部清理顺序
- 状态切换为
ENDING
- AI 正在生成时发送
response.cancel
- 停止麦克风 Track
- 停止播放器并清空缓冲
- 收集
RTCPeerConnection.getStats()
- 关闭 DataChannel
- 关闭 PeerConnection
- 上报 telemetry
- 调用后端 end
- 清空 SDK 状态并通知 UI
SDK → 后端:质量上报
POST /v1/free-chat/sessions/{sessionId}/telemetry
{
"metrics": {
"duration_seconds": 388,
"packet_loss_percent": 0.15,
"max_jitter_ms": 20,
"avg_rtt_ms": 105,
"connection_disruptions": 0,
"interrupt_count": 2,
"pause_count": 1
}
}
telemetry 失败不得阻止结束。
SDK → 后端:结束 Session
POST /v1/free-chat/sessions/{sessionId}/end
请求:
{
"reason": "USER_ENDED",
"client_ended_at": "2026-07-17T10:26:30Z",
"provider_session_id": "provider_session_xxx"
}
响应:
{
"session_id": "fcs_xxx",
"status": "ENDED",
"end_reason": "USER_ENDED",
"usage_status": "PENDING_PROVIDER_CONFIRMATION",
"estimated_usage": {
"connected_seconds": 388,
"active_seconds": 360,
"paused_seconds": 20
},
"content_delete_at": "2026-07-18T10:26:30Z"
}
end 必须幂等,重复调用不能重复扣费。
HTTP 接口清单
POST /v1/free-chat/sessions
POST /v1/free-chat/sessions/{sessionId}/sdp
POST /v1/free-chat/sessions/{sessionId}/provider-session
POST /v1/free-chat/sessions/{sessionId}/pause
POST /v1/free-chat/sessions/{sessionId}/resume
POST /v1/free-chat/sessions/{sessionId}/events
POST /v1/free-chat/sessions/{sessionId}/telemetry
POST /v1/free-chat/sessions/{sessionId}/end
SDK 方法清单
start()
pause()
resume()
interrupt()
end()
非目标
- 不做长期 Conversation 档案
- 不做跨 Session 记忆
- 不绑定 Pion
- 不通过业务后端实时转发音频
- 第一阶段不要求在原 Provider Session 上实现复杂断线续聊
详细接口设计
见UniSpeaking_自由对话模块五阶段接口设计.md
自由对话模块五阶段接口设计
背景
自由对话采用一次性 Session:
sessionId目标
为以下五个阶段定义统一接口:
涉及四个端:
一、开始
UI → SDK
SDK → 后端:创建 Session
请求:
{ "coach_id": "clara", "voice_id": "clara_default", "difficulty": "BEGINNER", "language": "en-US", "client": { "platform": "WEB", "app_version": "0.1.0", "device_id": "device_xxx" }, "idempotency_key": "start_xxx" }响应:
{ "session_id": "fcs_xxx", "status": "CREATED", "expires_at": "2026-07-17T10:30:00Z", "content_delete_at": "2026-07-18T10:30:00Z", "policy": { "max_duration_seconds": 600, "remaining_seconds": 600, "idle_timeout_seconds": 60 }, "realtime_config": { "voice": "clara_default", "modalities": ["text", "audio"], "turn_detection": { "type": "server_vad", "threshold": 0.5, "silence_duration_ms": 800 } } }SDK → 后端:交换 SDP
发送:Offer SDP
接收:Answer SDP
SDK → Qwen
收到:
{ "type": "session.created", "session": { "id": "provider_session_xxx" } }发送:
{ "type": "session.update", "session": { "instructions": "<server-generated-prompt>", "voice": "clara_default", "modalities": ["text", "audio"], "turn_detection": { "type": "server_vad", "threshold": 0.5, "prefix_padding_ms": 500, "silence_duration_ms": 800 } } }AI 主动开场:
{ "type": "response.create" }SDK → 后端:绑定厂商 Session
{ "provider_session_id": "provider_session_xxx" }二、暂停
UI → SDK
SDK 内部
response.cancelSDK → 后端
请求:
{ "reason": "USER_PAUSED", "client_paused_at": "2026-07-17T10:23:00Z" }响应:
{ "session_id": "fcs_xxx", "status": "PAUSED", "remaining_seconds": 420, "max_pause_seconds": 120 }三、恢复
UI → SDK
SDK → 后端
请求:
{ "client_resumed_at": "2026-07-17T10:23:20Z" }响应:
{ "session_id": "fcs_xxx", "status": "ACTIVE", "pause_duration_seconds": 20, "remaining_seconds": 400 }SDK 内部
response.create四、打断
UI / VAD → SDK
SDK → Qwen
{ "event_id": "evt_interrupt_xxx", "type": "response.cancel" }SDK 内部
response.interruptedSDK → 后端(可选)
{ "event_id": "evt_interrupt_xxx", "event_type": "ASSISTANT_RESPONSE_INTERRUPTED", "occurred_at": "2026-07-17T10:24:10Z", "payload": { "reason": "USER_SPEAKING", "provider_response_id": "resp_xxx" } }打断事件上报失败不得影响实时打断。
五、结束
UI → SDK
SDK 内部清理顺序
ENDINGresponse.cancelRTCPeerConnection.getStats()SDK → 后端:质量上报
{ "metrics": { "duration_seconds": 388, "packet_loss_percent": 0.15, "max_jitter_ms": 20, "avg_rtt_ms": 105, "connection_disruptions": 0, "interrupt_count": 2, "pause_count": 1 } }telemetry 失败不得阻止结束。
SDK → 后端:结束 Session
请求:
{ "reason": "USER_ENDED", "client_ended_at": "2026-07-17T10:26:30Z", "provider_session_id": "provider_session_xxx" }响应:
{ "session_id": "fcs_xxx", "status": "ENDED", "end_reason": "USER_ENDED", "usage_status": "PENDING_PROVIDER_CONFIRMATION", "estimated_usage": { "connected_seconds": 388, "active_seconds": 360, "paused_seconds": 20 }, "content_delete_at": "2026-07-18T10:26:30Z" }end必须幂等,重复调用不能重复扣费。HTTP 接口清单
SDK 方法清单
非目标
详细接口设计
见UniSpeaking_自由对话模块五阶段接口设计.md