rtc-voice-chat/backend/API.md
2026-04-02 20:15:15 +08:00

509 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Python 后端 API 文档
> Base URL: `http://localhost:3001`
>
> 所有来自 **java-mock** 的请求须附加内部服务签名 Header见[内部鉴权协议](#内部鉴权协议))。
>
> `/api/chat_callback` 由**火山引擎 RTC 平台**直接回调,走独立 API Key 鉴权。
---
## 目录
- [接口总览](#接口总览)
- [一、场景接口](#一场景接口)
- [1.1 获取场景列表](#11-获取场景列表)
- [二、RTC 代理接口](#二rtc-代理接口)
- [2.1 开始 / 停止语音对话](#21-开始--停止语音对话)
- [三、会话历史接口](#三会话历史接口)
- [3.1 写入历史上下文](#31-写入历史上下文)
- [四、LLM 回调接口](#四llm-回调接口)
- [4.1 自定义 LLM 回调SSE](#41-自定义-llm-回调sse)
- [五、调试接口](#五调试接口)
- [5.1 调试聊天](#51-调试聊天)
- [5.2 调试 RAG 检索](#52-调试-rag-检索)
- [内部鉴权协议](#内部鉴权协议)
- [通用错误结构](#通用错误结构)
- [环境变量](#环境变量)
---
## 接口总览
| 方法 | 路径 | 调用方 | 鉴权方式 | 说明 |
|---|---|---|---|---|
| POST | `/getScenes` | java-mock | 内部签名 | 获取场景列表 & RTC 配置 |
| POST | `/proxy` | java-mock | 内部签名 | 转发 StartVoiceChat / StopVoiceChat 到火山引擎 |
| POST | `/api/session/history` | java-mock | 内部签名 | 写入房间历史上下文 |
| POST | `/api/chat_callback` | 火山引擎 RTC 平台 | API Key | 自定义 LLM 回调,返回 SSE 流 |
| POST | `/debug/chat` | 开发调试 | 无 | 直接测试 LLM 对话 |
| GET | `/debug/rag` | 开发调试 | 无 | 测试 RAG 知识库检索 |
---
## 一、场景接口
### 1.1 获取场景列表
```
POST /getScenes
```
**鉴权:** 内部签名java-mock 调用)
**请求体:** `{}` 或空
**成功响应 200**
```json
{
"ResponseMetadata": {
"Action": "getScenes"
},
"Result": {
"scenes": [
{
"scene": {
"id": "Custom",
"botName": "BotUser001",
"isInterruptMode": true,
"isVision": false,
"isScreenMode": false,
"isAvatarScene": false,
"avatarBgUrl": null
},
"rtc": {
"AppId": "6xxxxxxx",
"RoomId": "550e8400-e29b-41d4-a716-446655440000",
"UserId": "user-xyz",
"Token": "AQBhMGI3Zm...",
"TaskId": "task-001"
}
}
]
}
}
```
**场景字段说明:**
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 场景唯一标识,后续接口的 `SceneID` 取此值 |
| botName | string | AI Bot 在 RTC 房间中的 UserId |
| isInterruptMode | boolean | 是否开启打断模式InterruptMode === 0 |
| isVision | boolean | 是否开启视觉能力 |
| isScreenMode | boolean | 是否为屏幕共享模式 |
| isAvatarScene | boolean | 是否为数字人场景 |
| avatarBgUrl | string\|null | 数字人背景图 URL |
**RTC 字段说明:**
| 字段 | 类型 | 说明 |
|---|---|---|
| AppId | string | 火山引擎 RTC AppId |
| RoomId | string | 本次会话分配的房间 IDUUID |
| UserId | string | 用户在 RTC 房间中的 UserId |
| Token | string | RTC 入房 Token |
| TaskId | string | 语音任务 IDStopVoiceChat 时需要 |
> **副作用:** 该接口会将 `RoomId / UserId / TaskId` 写入服务端 Session供后续 `/proxy?Action=StartVoiceChat` 自动取用,无需客户端传递。
**失败响应:**
```json
{
"ResponseMetadata": {
"Action": "getScenes",
"Error": {
"Code": -1,
"Message": "错误描述"
}
}
}
```
---
## 二、RTC 代理接口
### 2.1 开始 / 停止语音对话
```
POST /proxy?Action=<action>&Version=<version>
```
**鉴权:** 内部签名java-mock 调用)
**Query 参数:**
| 字段 | 必填 | 默认值 | 说明 |
|---|---|---|---|
| Action | ✅ | — | `StartVoiceChat``StopVoiceChat` |
| Version | ❌ | 配置文件值 | 火山引擎 OpenAPI 版本,如 `2024-12-01` |
**请求体JSON**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| SceneID | string | ✅ | 场景 ID`getScenes``scene.id` 获取) |
---
#### StartVoiceChat
**请求示例:**
```json
{
"SceneID": "Custom"
}
```
**内部处理逻辑:**
1. 从 Session 取回 `getScenes` 时生成的 `RoomId / UserId / TaskId`
2.`room_id` 附加到 LLM 回调 URL`?room_id=xxx`),使 `/api/chat_callback` 能关联历史上下文
3. 带 SigV4 签名转发到火山引擎 `https://rtc.volcengineapi.com`
**成功响应 200**
```json
{
"ResponseMetadata": {
"RequestId": "2024xxxxxxxxxx",
"Action": "StartVoiceChat",
"Version": "2024-12-01",
"Service": "rtc"
},
"Result": {
"Message": "success"
}
}
```
---
#### StopVoiceChat
**请求示例:**
```json
{
"SceneID": "Custom"
}
```
**内部处理逻辑:**
1. 从 Session 取回 `RoomId / TaskId`
2. 清除该房间的历史上下文缓存(`room_history`
3. 带 SigV4 签名转发到火山引擎
**成功响应 200**
```json
{
"ResponseMetadata": {
"RequestId": "2024xxxxxxxxxx",
"Action": "StopVoiceChat",
"Version": "2024-12-01",
"Service": "rtc"
},
"Result": {
"Message": "success"
}
}
```
**通用失败响应:**
```json
{
"ResponseMetadata": {
"Action": "StartVoiceChat",
"Error": {
"Code": "InvalidParameter",
"Message": "SceneID 不能为空SceneID 用于指定场景配置"
}
}
}
```
---
## 三、会话历史接口
### 3.1 写入历史上下文
> 在 `StartVoiceChat` 之前调用,将历史对话注入该房间的上下文缓存。后续 `/api/chat_callback` 会自动将其 prepend 到每次 LLM 请求的 messages 前。
```
POST /api/session/history
```
**鉴权:** 内部签名java-mock 调用)
**请求体JSON**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| room_id | string | ✅ | RTC 房间 ID`getScenes` 返回的 `rtc.RoomId` 一致) |
| messages | array | ✅ | 历史消息数组 |
`messages` 每条消息:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| role | string | ✅ | `"user"``"assistant"` |
| content | string | ✅ | 消息文本 |
**请求示例:**
```json
{
"room_id": "550e8400-e29b-41d4-a716-446655440000",
"messages": [
{ "role": "assistant", "content": "你好,我是小块" },
{ "role": "user", "content": "今天出勤情况咋样" },
{ "role": "assistant", "content": "今天出勤率 90%" }
]
}
```
**成功响应 200**
```json
{
"code": 200
}
```
**失败响应 401**
```json
{
"code": 401,
"message": "鉴权失败"
}
```
---
## 四、LLM 回调接口
### 4.1 自定义 LLM 回调SSE
> 由**火山引擎 RTC 平台**在用户发言后自动回调。返回 OpenAI 兼容格式的 SSE 流。
```
POST /api/chat_callback?room_id=<room_id>
```
**鉴权:** `Authorization: Bearer <CUSTOM_LLM_API_KEY>`Header 或 Query 参数)
**Query 参数:**
| 字段 | 必填 | 说明 |
|---|---|---|
| room_id | ❌ | 房间 ID。传入时自动从缓存取历史上下文并 prepend 到 messages |
**请求体JSON**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| messages | array | ✅ | 对话消息列表,最后一条必须是 `user` 角色 |
| temperature | float | ❌ | 采样温度 |
| max_tokens | int | ❌ | 最大生成 token 数 |
| top_p | float | ❌ | Top-P 采样 |
`messages` 中每条消息:
| 字段 | 类型 | 说明 |
|---|---|---|
| role | string | `"user"` / `"assistant"` / `"system"` |
| content | string | 消息文本 |
**请求示例:**
```json
{
"messages": [
{ "role": "user", "content": "今天办公室出勤情况咋样" }
],
"temperature": 0.7,
"max_tokens": 1024
}
```
**处理逻辑:**
1. 验证 API Key
2. 过滤掉 `content === "欢迎语"` 的触发词消息RTC 平台自动发送,非真实用户输入)
3. 若有 `room_id`,从缓存取历史并 prepend 到 messages 前
4. 调用本地 LLM 服务(工具调用 / RAG 按需触发)
5. 以 SSE 流返回结果
**成功响应 200text/event-stream**
```
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"今"},...}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"天"},...}]}
data: [DONE]
```
**失败响应SSE 格式HTTP 状态码对应):**
```
data: {"error":{"code":"AuthenticationError","message":"API Key 无效"}}
data: [DONE]
```
| HTTP 状态码 | code | 触发场景 |
|---|---|---|
| 401 | `AuthenticationError` | API Key 无效 |
| 400 | `BadRequest` | messages 为空 / 最后一条不是 user |
| 500 | `InternalError` | LLM 初始化失败 / 请求解析失败 |
---
## 五、调试接口
> 仅供本地开发使用,无鉴权。
### 5.1 调试聊天
```
POST /debug/chat
```
直接发消息给 LLM响应为纯文本流非 SSE。完成后在服务端终端输出可复用的 `history` JSON 结构。
**请求体JSON**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| history | array | ❌ | 历史消息列表,格式同 `messages`,默认空 |
| question | string | ✅ | 本次用户提问 |
**请求示例:**
```json
{
"history": [
{ "role": "assistant", "content": "你好,有什么可以帮你?" }
],
"question": "今天办公室有多少人打卡?"
}
```
**成功响应 200text/plain 流式):**
```
今天办公室一共九人,目前出勤率为 100%。
```
---
### 5.2 调试 RAG 检索
```
GET /debug/rag?query=<query>
```
测试知识库检索,返回检索到的原始上下文内容。
**Query 参数:**
| 字段 | 必填 | 说明 |
|---|---|---|
| query | ✅ | 检索问题 |
**成功响应 200**
```json
{
"query": "今天出勤情况",
"retrieved_context": "#### 今天出勤数据\n办公室共 9 人...",
"length": 128,
"status": "success"
}
```
无结果时 `status``"no_results_or_error"``retrieved_context` 为 `null`
---
## 内部鉴权协议
> `/getScenes`、`/proxy`、`/api/session/history` 均启用此鉴权。
**java-mock 发送方**在请求头附加:
| Header | 说明 |
|---|---|
| `X-Internal-Service` | 固定值 `java-gateway` |
| `X-Internal-User-Id` | 当前登录用户 ID |
| `X-Internal-Timestamp` | 毫秒级 Unix 时间戳(字符串) |
| `X-Internal-Signature` | HMAC-SHA256 签名hex |
| `X-User-Name` | URL 编码的用户显示名 |
| `X-User-Sex` | 性别 |
| `X-User-Is-Driver` | `"true"` / `"false"` |
| `X-User-Dept-Id` | 部门 ID |
| `X-User-Dept-Name` | URL 编码的部门名称 |
| `X-User-Role-List` | URL 编码的角色列表 JSON |
**签名算法:**
```
message = "java-gateway:{userId}:{毫秒时间戳}"
signature = HMAC-SHA256(INTERNAL_SERVICE_SECRET, message) // hex
```
**Python 接收方验证逻辑(`security/internal_auth.py`**
1. `INTERNAL_SERVICE_SECRET` 未配置 → 直接放行(开发环境兼容)
2. 校验 4 个必要字段是否存在,`X-Internal-Service` 必须为 `java-gateway`
3. 时间窗口:`abs(now_ms - timestamp) ≤ 5分钟`(防重放)
4. 重新计算 HMAC`hmac.compare_digest` 常量时间比较(防时序攻击)
---
## 通用错误结构
**内部接口getScenes / proxy / session 系列)** 返回火山引擎风格:
```json
{
"ResponseMetadata": {
"Action": "xxx",
"Error": {
"Code": "InvalidParameter",
"Message": "SceneID 不能为空"
}
}
}
```
**chat_callback** 返回 SSE 格式错误:
```
data: {"error":{"code":"BadRequest","message":"messages 不能为空"}}
data: [DONE]
```
---
## 环境变量
| 变量名 | 必填 | 说明 |
|---|---|---|
| `INTERNAL_SERVICE_SECRET` | ✅ | 内部服务签名密钥,与 java-mock 侧保持一致 |
| `CUSTOM_LLM_API_KEY` | ✅ | `chat_callback` 接口的鉴权 Key配置到火山引擎 RTC 场景中 |
| `OPENAI_API_KEY` | ✅(或同类) | 接入 LLM 所需的 API Key |
| `OPENAI_BASE_URL` | ❌ | 自定义 LLM Base URL接入本地模型时使用 |
| `LLM_MODEL` | ❌ | 指定模型名称 |
| `RAG_*` | ❌ | 知识库相关配置(详见 `utils/env.py` |