rtc-voice-chat/backend/API.md

495 lines
13 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/v1`
>
> 所有路径均挂载在 `/v1` 前缀下,完整路径如 `POST http://localhost:3001/v1/getScenes`。
>
> 来自 **java-mock** 的请求须附加 [内部签名 Header](#内部鉴权协议)。
>
> `/chat_callback` 由**火山引擎 RTC 平台**直接回调,走独立 API Key 鉴权。
---
## 接口总览
| 方法 | 路径 | 调用方 | 鉴权 | 说明 |
|------|------|--------|------|------|
| POST | `/getScenes` | java-mock | 内部签名 | 获取场景列表 & RTC 入房参数 |
| POST | `/proxy?Action=xxx` | java-mock | 内部签名 | 开始/停止语音对话 |
| POST | `/session/history` | java-mock | 内部签名 | 写入房间历史上下文 |
| POST | `/chat_callback` | 火山引擎 RTC | Bearer Token | 自定义 LLM 回调SSE 流式) |
| POST | `/debug/chat` | 开发调试 | 无 | 调试 LLM 对话 |
| GET | `/debug/rag` | 开发调试 | 无 | 调试 RAG 检索 |
### 调用时序
```
前端 → java-mock → Python 后端
1. POST /getScenes ← 获取场景 + RTC 入房参数
2. POST /session/history ← (可选) 注入历史上下文
3. POST /proxy?Action=StartVoiceChat ← 启动语音对话
4. 火山引擎 RTC → POST /chat_callback ← 平台回调(自动,非 java-mock
5. POST /proxy?Action=StopVoiceChat ← 结束语音对话
```
---
## 一、获取场景列表
```
POST /v1/getScenes
```
返回所有已配置场景的信息和对应的 RTC 入房参数。每次调用会**重新生成** RoomId/UserId/Token/TaskId 并写入服务端 Session供后续 `StartVoiceChat` 自动取用。
### 请求
**Headers内部签名**
| Header | 必填 | 说明 |
|--------|------|------|
| `X-Internal-Service` | ✅ | 固定 `java-gateway` |
| `X-Internal-User-Id` | ✅ | 当前登录用户 ID |
| `X-Internal-Timestamp` | ✅ | 毫秒时间戳(字符串) |
| `X-Internal-Signature` | ✅ | HMAC-SHA256 签名hex算法见[内部鉴权协议](#内部鉴权协议) |
**Body** 无(`{}` 或空均可)
### 成功响应 `200`
```json
{
"ResponseMetadata": {
"Action": "getScenes"
},
"Result": {
"scenes": [
{
"scene": {
"id": "Custom",
"name": "小块",
"icon": "https://lf3-rtc-demo.volccdn.com/obj/rtc-aigc-assets/DoubaoAvatar.png",
"botName": "agent-user-001",
"isInterruptMode": true,
"isVision": false,
"isScreenMode": false,
"isAvatarScene": false,
"avatarBgUrl": ""
},
"rtc": {
"AppId": "6xxxxxxx",
"RoomId": "550e8400-e29b-41d4-a716-446655440000",
"UserId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"Token": "001xxxxxxAQBhMGI3Zm...",
"TaskId": "e5f6a7b8-1234-5678-9abc-def012345678"
}
}
]
}
}
```
**`Result.scenes[*].scene` 字段说明:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 场景唯一标识,后续 `/proxy` 接口的 `SceneID` 取此值 |
| `name` | string | 场景显示名称 |
| `icon` | string | 场景头像 URL |
| `botName` | string | AI Bot 在 RTC 房间中的 UserId |
| `isInterruptMode` | boolean | 是否开启用户打断模式 |
| `isVision` | boolean | 是否开启视觉理解能力 |
| `isScreenMode` | boolean | 是否为屏幕共享模式(视觉输入来自屏幕流) |
| `isAvatarScene` | boolean | 是否为数字人场景 |
| `avatarBgUrl` | string\|null | 数字人背景图 URL非数字人场景为空 |
**`Result.scenes[*].rtc` 字段说明:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `AppId` | string | 火山引擎 RTC AppId |
| `RoomId` | string | 本次生成的房间 IDUUID前端入房和后续接口都需要 |
| `UserId` | string | 本次生成的用户 IDUUID前端入房使用 |
| `Token` | string | RTC 入房 Token24 小时有效 |
| `TaskId` | string | 语音任务 ID`StopVoiceChat` 时需要 |
> **注意:** 每次调用 `getScenes` 都会重新生成 `RoomId`/`UserId`/`Token`/`TaskId`。必须在 `getScenes` 之后、`StartVoiceChat` 之前使用同一套参数。
### 失败响应
**鉴权失败 `401`**
```json
{
"code": 401,
"message": "鉴权失败"
}
```
**配置错误(缺少环境变量等):**
```json
{
"ResponseMetadata": {
"Action": "getScenes",
"Error": {
"Code": -1,
"Message": "Custom 场景缺少以下环境变量: CUSTOM_ACCESS_KEY_ID, CUSTOM_SECRET_KEY"
}
}
}
```
---
## 二、开始/停止语音对话
```
POST /v1/proxy?Action={Action}&Version={Version}
```
带 SigV4 签名转发到火山引擎 RTC OpenAPI。内部会自动从 Session 取回 `getScenes` 分配的房间参数。
### 请求
**Headers内部签名** 同 [getScenes](#请求)
**Query 参数:**
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `Action` | string | ✅ | — | `StartVoiceChat``StopVoiceChat` |
| `Version` | string | ❌ | 环境变量 `RTC_OPENAPI_VERSION`,兜底 `2025-06-01` | 火山引擎 OpenAPI 版本 |
**BodyJSON**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `SceneID` | string | ✅ | 场景 ID`getScenes` 返回的 `scene.id`(当前固定为 `"Custom"` |
**请求示例:**
```json
{
"SceneID": "Custom"
}
```
### StartVoiceChat 处理逻辑
1. 从 Session 取回 `getScenes` 时生成的 `RoomId`、`UserId`、`TaskId`
2.`room_id` 追加到 LLM 回调 URL`?room_id={RoomId}`),使 `/chat_callback` 能关联历史上下文
3. 使用 AK/SK 对请求做 SigV4 签名
4. 转发到 `https://rtc.volcengineapi.com?Action=StartVoiceChat`
### StartVoiceChat 成功响应 `200`
```json
{
"ResponseMetadata": {
"RequestId": "2025070100000000000000000000abcd",
"Action": "StartVoiceChat",
"Version": "2025-06-01",
"Service": "rtc"
},
"Result": {
"Message": "success"
}
}
```
### StopVoiceChat 处理逻辑
1. 从 Session 取回 `RoomId`、`TaskId`
2. **清除该房间的历史上下文缓存**`session/history` 写入的数据)
3. 使用 AK/SK 签名后转发到火山引擎
### StopVoiceChat 成功响应 `200`
```json
{
"ResponseMetadata": {
"RequestId": "2025070100000000000000000000efgh",
"Action": "StopVoiceChat",
"Version": "2025-06-01",
"Service": "rtc"
},
"Result": {
"Message": "success"
}
}
```
### 失败响应
**鉴权失败 `401`**
```json
{
"code": 401,
"message": "鉴权失败"
}
```
**参数/配置错误HTTP 200但包含 Error**
```json
{
"ResponseMetadata": {
"Action": "StartVoiceChat",
"Error": {
"Code": -1,
"Message": "SceneID 不能为空SceneID 用于指定场景配置"
}
}
}
```
| 错误场景 | Error.Message |
|----------|---------------|
| Action 为空 | `Action 不能为空` |
| SceneID 为空 | `SceneID 不能为空SceneID 用于指定场景配置` |
| 场景不存在 | `{SceneID} 不存在,请先配置对应场景。` |
| AK/SK 缺失 | `Custom 场景的 AccountConfig.accessKeyId 不能为空` |
| 火山引擎接口报错 | 透传火山引擎原始错误信息 |
---
## 三、写入历史上下文
```
POST /v1/session/history
```
`StartVoiceChat` **之前**调用,将上一次的对话历史注入该房间的上下文缓存。后续火山引擎 RTC 回调 `/chat_callback` 时,会自动将这些历史消息 prepend 到每次 LLM 请求的 messages 前面,实现跨会话的上下文延续。
### 请求
**Headers内部签名** 同 [getScenes](#请求)
**BodyJSON**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `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": "今天全部门出勤率百分之九十五,共二十三人到岗。" }
]
}
```
### 成功响应 `200`
```json
{
"code": 200
}
```
### 失败响应
**鉴权失败 `401`**
```json
{
"code": 401,
"message": "鉴权失败"
}
```
**Body 校验失败 `422`FastAPI 自动校验):**
```json
{
"detail": [
{
"type": "missing",
"loc": ["body", "room_id"],
"msg": "Field required"
}
]
}
```
> **注意:**
> - 每次调用会**覆盖**该 `room_id` 下已有的历史,不是追加
> - `StopVoiceChat` 时会自动清除该房间的历史缓存
> - 如果不需要上下文延续(新对话),可以跳过此接口
---
## 四、自定义 LLM 回调SSE
> 此接口由**火山引擎 RTC 平台**自动回调,不经过 java-mock。
```
POST /v1/chat_callback?room_id={room_id}
```
**鉴权:** `Authorization: Bearer <CUSTOM_LLM_API_KEY>`
**Query 参数:**
| 参数 | 必填 | 说明 |
|------|------|------|
| `room_id` | ❌ | 房间 ID`StartVoiceChat` 自动追加到回调 URL 中 |
**BodyJSON**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `messages` | array | ✅ | 对话消息列表,最后一条必须是 `user` 角色 |
| `temperature` | float | ❌ | 采样温度 |
| `max_tokens` | int | ❌ | 最大生成 token 数 |
| `top_p` | float | ❌ | Top-P 采样 |
**成功响应 `200``text/event-stream`**
```
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"今"}}]}
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"天"}}]}
data: [DONE]
```
**失败响应:**
| HTTP 状态码 | Error.Code | 触发场景 |
|-------------|------------|----------|
| 401 | `AuthenticationError` | API Key 无效 |
| 400 | `BadRequest` | messages 为空 / 最后一条不是 user |
| 500 | `InternalError` | LLM 初始化失败 / 请求解析失败 |
---
## 五、调试接口
> 仅供本地开发,无鉴权。
### 5.1 调试聊天
```
POST /v1/debug/chat
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `history` | array | ❌ | 历史消息列表(`role` + `content`),默认空 |
| `question` | string | ✅ | 本次用户提问 |
**响应:** `200 text/plain` 流式文本
### 5.2 调试 RAG 检索
```
GET /v1/debug/rag?query={query}
```
| 参数 | 必填 | 说明 |
|------|------|------|
| `query` | ✅ | 检索问题 |
**响应 `200`**
```json
{
"query": "今天出勤情况",
"retrieved_context": "检索到的知识文本...",
"length": 128,
"status": "success"
}
```
---
## 内部鉴权协议
> `/getScenes`、`/proxy`、`/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 编码的用户显示名透传Python 侧暂未使用) |
| `X-User-Sex` | ❌ | 性别 |
| `X-User-Dept-Name` | ❌ | URL 编码的部门名称 |
**签名算法:**
```
message = "java-gateway:{userId}:{毫秒时间戳}"
signature = HMAC-SHA256(INTERNAL_SERVICE_SECRET, message) → hex 编码
```
**Python 接收方验证逻辑:**
1. `INTERNAL_SERVICE_SECRET` 未配置 → **直接放行**(开发环境兼容)
2. 校验 `X-Internal-Service` 必须为 `java-gateway`,且 4 个必要 Header 非空
3. 时间窗口校验:`|当前时间 - timestamp| ≤ 5 分钟`(防重放)
4. 重新计算 HMAC`hmac.compare_digest` 常量时间比较(防时序攻击)
---
## 通用错误结构
**内部接口getScenes / proxy / session/history** 返回火山引擎风格:
```json
{
"ResponseMetadata": {
"Action": "操作名",
"Error": {
"Code": -1,
"Message": "错误描述"
}
}
}
```
**chat_callback** 返回 JSON 格式错误(非 SSE
```json
{
"Error": {
"Code": "BadRequest",
"Message": "messages 不能为空"
}
}
```
---
## 环境变量
| 变量名 | 必填 | 说明 |
|--------|------|------|
| `INTERNAL_SERVICE_SECRET` | ✅ | 内部服务签名密钥,需与 java-mock 侧一致 |
| `CUSTOM_LLM_API_KEY` | ❌ | `/chat_callback` 的 Bearer Token留空则跳过鉴权 |
| `LOCAL_LLM_API_KEY` | ✅ | 方舟 LLM API Key |
| `LOCAL_LLM_MODEL` | ✅ | 方舟端点 ID |
| `LOCAL_LLM_BASE_URL` | ✅ | 方舟 API 地址 |
完整变量列表见 `.env.example`