refactor: update API endpoints to use consistent path structure, changing /api/chat_callback to /chat_callback and adjusting related documentation and configurations accordingly.
This commit is contained in:
parent
271f7345c9
commit
6daaae00d3
@ -9,4 +9,5 @@ __pycache__/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
*.md
|
||||
!prompts/*.md
|
||||
*.log
|
||||
|
||||
@ -20,14 +20,14 @@ CUSTOM_AGENT_WELCOME_MESSAGE=你好,我是小块,有什么需要帮忙的吗
|
||||
CUSTOM_INTERRUPT_MODE=0
|
||||
|
||||
# ============ LLM 配置 (RTC OpenAPI 侧) ============
|
||||
# RTC 会回调 CUSTOM_LLM_URL 指定的地址(通常是本后端的 /api/chat_callback)
|
||||
# RTC 会回调 CUSTOM_LLM_URL 指定的地址(通常是本后端的 /v1/chat_callback)
|
||||
CUSTOM_LLM_THINKING_TYPE=disabled
|
||||
CUSTOM_LLM_VISION_ENABLE=false
|
||||
|
||||
# 填写后端服务的公网 HTTPS 地址,火山引擎 RTC 平台会回调此地址
|
||||
# 例如:https://api.yourdomain.com/v1/api/chat_callback
|
||||
# 例如:https://api.yourdomain.com/v1/chat_callback
|
||||
CUSTOM_LLM_URL=
|
||||
# 火山调用当前 backend 的 /api/chat_callback 时使用的 Bearer Token,可留空
|
||||
# 火山调用当前 backend 的 /v1/chat_callback 时使用的 Bearer Token,可留空
|
||||
CUSTOM_LLM_API_KEY=
|
||||
CUSTOM_LLM_MODEL_NAME=
|
||||
CUSTOM_LLM_HISTORY_LENGTH=
|
||||
@ -39,8 +39,8 @@ CUSTOM_LLM_TEMPERATURE=
|
||||
CUSTOM_LLM_TOP_P=
|
||||
CUSTOM_LLM_MAX_TOKENS=
|
||||
|
||||
# ============ 本地 LLM 回调配置 (/api/chat_callback) ============
|
||||
# RTC 回调本后端的 /api/chat_callback 后,
|
||||
# ============ 本地 LLM 回调配置 (/v1/chat_callback) ============
|
||||
# RTC 回调本后端的 /v1/chat_callback 后,
|
||||
# 本后端再用以下配置调用方舟(通过 OpenAI SDK)。
|
||||
LOCAL_LLM_API_KEY=
|
||||
LOCAL_LLM_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||
@ -90,3 +90,8 @@ CUSTOM_AVATAR_TOKEN=
|
||||
# 启用后 LLM 可主动调用已注册的工具函数查询真实数据
|
||||
TOOLS_ENABLED=true
|
||||
TOOLS_MAX_ROUNDS=5 # 单次对话最大工具调用轮数(防无限循环)
|
||||
|
||||
# ============ API Tools(业务接口工具) ============
|
||||
API_TOOLS_BASE_URL=https://hskuaikuai.com:9000/test
|
||||
API_TOOLS_TOKEN=
|
||||
API_TOOLS_TIMEOUT=10
|
||||
|
||||
@ -25,7 +25,7 @@ CUSTOM_INTERRUPT_MODE=0
|
||||
|
||||
# ============ LLM 配置 (RTC OpenAPI 侧) ============
|
||||
# 测试环境回调地址 —— 改成 staging 服务器的公网地址
|
||||
CUSTOM_LLM_URL=https://your-staging-domain.example.com/v1/api/chat_callback
|
||||
CUSTOM_LLM_URL=https://your-staging-domain.example.com/v1/chat_callback
|
||||
CUSTOM_LLM_API_KEY=your-staging-llm-api-key
|
||||
CUSTOM_LLM_THINKING_TYPE=disabled
|
||||
CUSTOM_LLM_VISION_ENABLE=false
|
||||
|
||||
680
backend/API.md
680
backend/API.md
@ -1,59 +1,62 @@
|
||||
# Python 后端 API 文档
|
||||
|
||||
> Base URL: `http://localhost:3001`
|
||||
> Base URL: `http://localhost:3001/v1`
|
||||
>
|
||||
> 所有来自 **java-mock** 的请求须附加内部服务签名 Header(见[内部鉴权协议](#内部鉴权协议))。
|
||||
> 所有路径均挂载在 `/v1` 前缀下,完整路径如 `POST http://localhost:3001/v1/getScenes`。
|
||||
>
|
||||
> `/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-检索)
|
||||
- [内部鉴权协议](#内部鉴权协议)
|
||||
- [通用错误结构](#通用错误结构)
|
||||
- [环境变量](#环境变量)
|
||||
> 来自 **java-mock** 的请求须附加 [内部签名 Header](#内部鉴权协议)。
|
||||
>
|
||||
> `/chat_callback` 由**火山引擎 RTC 平台**直接回调,走独立 API Key 鉴权。
|
||||
|
||||
---
|
||||
|
||||
## 接口总览
|
||||
|
||||
| 方法 | 路径 | 调用方 | 鉴权方式 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| 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 知识库检索 |
|
||||
| 方法 | 路径 | 调用方 | 鉴权 | 说明 |
|
||||
|------|------|--------|------|------|
|
||||
| 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 ← 结束语音对话
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、场景接口
|
||||
|
||||
### 1.1 获取场景列表
|
||||
## 一、获取场景列表
|
||||
|
||||
```
|
||||
POST /getScenes
|
||||
POST /v1/getScenes
|
||||
```
|
||||
|
||||
**鉴权:** 内部签名(java-mock 调用)
|
||||
返回所有已配置场景的信息和对应的 RTC 入房参数。每次调用会**重新生成** RoomId/UserId/Token/TaskId 并写入服务端 Session,供后续 `StartVoiceChat` 自动取用。
|
||||
|
||||
**请求体:** `{}` 或空
|
||||
### 请求
|
||||
|
||||
**成功响应 200:**
|
||||
**Headers(内部签名):**
|
||||
|
||||
| Header | 必填 | 说明 |
|
||||
|--------|------|------|
|
||||
| `X-Internal-Service` | ✅ | 固定 `java-gateway` |
|
||||
| `X-Internal-User-Id` | ✅ | 当前登录用户 ID |
|
||||
| `X-Internal-Timestamp` | ✅ | 毫秒时间戳(字符串) |
|
||||
| `X-Internal-Signature` | ✅ | HMAC-SHA256 签名(hex),算法见[内部鉴权协议](#内部鉴权协议) |
|
||||
|
||||
**Body:** 无(`{}` 或空均可)
|
||||
|
||||
### 成功响应 `200`
|
||||
|
||||
```json
|
||||
{
|
||||
@ -65,19 +68,21 @@ POST /getScenes
|
||||
{
|
||||
"scene": {
|
||||
"id": "Custom",
|
||||
"botName": "BotUser001",
|
||||
"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": null
|
||||
"avatarBgUrl": ""
|
||||
},
|
||||
"rtc": {
|
||||
"AppId": "6xxxxxxx",
|
||||
"RoomId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"UserId": "user-xyz",
|
||||
"Token": "AQBhMGI3Zm...",
|
||||
"TaskId": "task-001"
|
||||
"UserId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
|
||||
"Token": "001xxxxxxAQBhMGI3Zm...",
|
||||
"TaskId": "e5f6a7b8-1234-5678-9abc-def012345678"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -85,199 +90,35 @@ POST /getScenes
|
||||
}
|
||||
```
|
||||
|
||||
**场景字段说明:**
|
||||
**`Result.scenes[*].scene` 字段说明:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | string | 场景唯一标识,后续接口的 `SceneID` 取此值 |
|
||||
| botName | string | AI Bot 在 RTC 房间中的 UserId |
|
||||
| isInterruptMode | boolean | 是否开启打断模式(InterruptMode === 0) |
|
||||
| isVision | boolean | 是否开启视觉能力 |
|
||||
| isScreenMode | boolean | 是否为屏幕共享模式 |
|
||||
| isAvatarScene | boolean | 是否为数字人场景 |
|
||||
| avatarBgUrl | string\|null | 数字人背景图 URL |
|
||||
|------|------|------|
|
||||
| `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,非数字人场景为空 |
|
||||
|
||||
**RTC 字段说明:**
|
||||
**`Result.scenes[*].rtc` 字段说明:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| AppId | string | 火山引擎 RTC AppId |
|
||||
| RoomId | string | 本次会话分配的房间 ID(UUID) |
|
||||
| UserId | string | 用户在 RTC 房间中的 UserId |
|
||||
| Token | string | RTC 入房 Token |
|
||||
| TaskId | string | 语音任务 ID,StopVoiceChat 时需要 |
|
||||
|------|------|------|
|
||||
| `AppId` | string | 火山引擎 RTC AppId |
|
||||
| `RoomId` | string | 本次生成的房间 ID(UUID),前端入房和后续接口都需要 |
|
||||
| `UserId` | string | 本次生成的用户 ID(UUID),前端入房使用 |
|
||||
| `Token` | string | RTC 入房 Token,24 小时有效 |
|
||||
| `TaskId` | string | 语音任务 ID,`StopVoiceChat` 时需要 |
|
||||
|
||||
> **副作用:** 该接口会将 `RoomId / UserId / TaskId` 写入服务端 Session,供后续 `/proxy?Action=StartVoiceChat` 自动取用,无需客户端传递。
|
||||
> **注意:** 每次调用 `getScenes` 都会重新生成 `RoomId`/`UserId`/`Token`/`TaskId`。必须在 `getScenes` 之后、`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:**
|
||||
**鉴权失败 `401`:**
|
||||
|
||||
```json
|
||||
{
|
||||
@ -286,82 +127,253 @@ POST /api/session/history
|
||||
}
|
||||
```
|
||||
|
||||
**配置错误(缺少环境变量等):**
|
||||
|
||||
```json
|
||||
{
|
||||
"ResponseMetadata": {
|
||||
"Action": "getScenes",
|
||||
"Error": {
|
||||
"Code": -1,
|
||||
"Message": "Custom 场景缺少以下环境变量: CUSTOM_ACCESS_KEY_ID, CUSTOM_SECRET_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、LLM 回调接口
|
||||
|
||||
### 4.1 自定义 LLM 回调(SSE)
|
||||
|
||||
> 由**火山引擎 RTC 平台**在用户发言后自动回调。返回 OpenAI 兼容格式的 SSE 流。
|
||||
## 二、开始/停止语音对话
|
||||
|
||||
```
|
||||
POST /api/chat_callback?room_id=<room_id>
|
||||
POST /v1/proxy?Action={Action}&Version={Version}
|
||||
```
|
||||
|
||||
**鉴权:** `Authorization: Bearer <CUSTOM_LLM_API_KEY>`(Header 或 Query 参数)
|
||||
带 SigV4 签名转发到火山引擎 RTC OpenAPI。内部会自动从 Session 取回 `getScenes` 分配的房间参数。
|
||||
|
||||
### 请求
|
||||
|
||||
**Headers(内部签名):** 同 [getScenes](#请求)
|
||||
|
||||
**Query 参数:**
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| room_id | ❌ | 房间 ID。传入时自动从缓存取历史上下文并 prepend 到 messages |
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| `Action` | string | ✅ | — | `StartVoiceChat` 或 `StopVoiceChat` |
|
||||
| `Version` | string | ❌ | 环境变量 `RTC_OPENAPI_VERSION`,兜底 `2025-06-01` | 火山引擎 OpenAPI 版本 |
|
||||
|
||||
**请求体(JSON):**
|
||||
**Body(JSON):**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| messages | array | ✅ | 对话消息列表,最后一条必须是 `user` 角色 |
|
||||
| temperature | float | ❌ | 采样温度 |
|
||||
| max_tokens | int | ❌ | 最大生成 token 数 |
|
||||
| top_p | float | ❌ | Top-P 采样 |
|
||||
|
||||
`messages` 中每条消息:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| role | string | `"user"` / `"assistant"` / `"system"` |
|
||||
| content | string | 消息文本 |
|
||||
|------|------|------|------|
|
||||
| `SceneID` | string | ✅ | 场景 ID,取 `getScenes` 返回的 `scene.id`(当前固定为 `"Custom"`) |
|
||||
|
||||
**请求示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{ "role": "user", "content": "今天办公室出勤情况咋样" }
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 1024
|
||||
"SceneID": "Custom"
|
||||
}
|
||||
```
|
||||
|
||||
**处理逻辑:**
|
||||
### StartVoiceChat 处理逻辑
|
||||
|
||||
1. 验证 API Key
|
||||
2. 过滤掉 `content === "欢迎语"` 的触发词消息(RTC 平台自动发送,非真实用户输入)
|
||||
3. 若有 `room_id`,从缓存取历史并 prepend 到 messages 前
|
||||
4. 调用本地 LLM 服务(工具调用 / RAG 按需触发)
|
||||
5. 以 SSE 流返回结果
|
||||
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`
|
||||
|
||||
**成功响应 200(text/event-stream):**
|
||||
### 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 不能为空` |
|
||||
| 火山引擎接口报错 | 透传火山引擎原始错误信息 |
|
||||
|
||||
---
|
||||
|
||||
## 三、写入历史上下文
|
||||
|
||||
```
|
||||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"今"},...}]}
|
||||
POST /v1/session/history
|
||||
```
|
||||
|
||||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"天"},...}]}
|
||||
在 `StartVoiceChat` **之前**调用,将上一次的对话历史注入该房间的上下文缓存。后续火山引擎 RTC 回调 `/chat_callback` 时,会自动将这些历史消息 prepend 到每次 LLM 请求的 messages 前面,实现跨会话的上下文延续。
|
||||
|
||||
### 请求
|
||||
|
||||
**Headers(内部签名):** 同 [getScenes](#请求)
|
||||
|
||||
**Body(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": "今天全部门出勤率百分之九十五,共二十三人到岗。" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 成功响应 `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 中 |
|
||||
|
||||
**Body(JSON):**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `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]
|
||||
```
|
||||
|
||||
**失败响应(SSE 格式,HTTP 状态码对应):**
|
||||
**失败响应:**
|
||||
|
||||
```
|
||||
data: {"error":{"code":"AuthenticationError","message":"API Key 无效"}}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
| HTTP 状态码 | code | 触发场景 |
|
||||
|---|---|---|
|
||||
| HTTP 状态码 | Error.Code | 触发场景 |
|
||||
|-------------|------------|----------|
|
||||
| 401 | `AuthenticationError` | API Key 无效 |
|
||||
| 400 | `BadRequest` | messages 为空 / 最后一条不是 user |
|
||||
| 500 | `InternalError` | LLM 初始化失败 / 请求解析失败 |
|
||||
@ -370,128 +382,101 @@ data: [DONE]
|
||||
|
||||
## 五、调试接口
|
||||
|
||||
> 仅供本地开发使用,无鉴权。
|
||||
> 仅供本地开发,无鉴权。
|
||||
|
||||
### 5.1 调试聊天
|
||||
|
||||
```
|
||||
POST /debug/chat
|
||||
POST /v1/debug/chat
|
||||
```
|
||||
|
||||
直接发消息给 LLM,响应为纯文本流(非 SSE)。完成后在服务端终端输出可复用的 `history` JSON 结构。
|
||||
|
||||
**请求体(JSON):**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| history | array | ❌ | 历史消息列表,格式同 `messages`,默认空 |
|
||||
| question | string | ✅ | 本次用户提问 |
|
||||
|------|------|------|------|
|
||||
| `history` | array | ❌ | 历史消息列表(`role` + `content`),默认空 |
|
||||
| `question` | string | ✅ | 本次用户提问 |
|
||||
|
||||
**请求示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"history": [
|
||||
{ "role": "assistant", "content": "你好,有什么可以帮你?" }
|
||||
],
|
||||
"question": "今天办公室有多少人打卡?"
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应 200(text/plain 流式):**
|
||||
|
||||
```
|
||||
今天办公室一共九人,目前出勤率为 100%。
|
||||
```
|
||||
|
||||
---
|
||||
**响应:** `200 text/plain` 流式文本
|
||||
|
||||
### 5.2 调试 RAG 检索
|
||||
|
||||
```
|
||||
GET /debug/rag?query=<query>
|
||||
GET /v1/debug/rag?query={query}
|
||||
```
|
||||
|
||||
测试知识库检索,返回检索到的原始上下文内容。
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `query` | ✅ | 检索问题 |
|
||||
|
||||
**Query 参数:**
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| query | ✅ | 检索问题 |
|
||||
|
||||
**成功响应 200:**
|
||||
**响应 `200`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "今天出勤情况",
|
||||
"retrieved_context": "#### 今天出勤数据\n办公室共 9 人...",
|
||||
"retrieved_context": "检索到的知识文本...",
|
||||
"length": 128,
|
||||
"status": "success"
|
||||
}
|
||||
```
|
||||
|
||||
无结果时 `status` 为 `"no_results_or_error"`,`retrieved_context` 为 `null`。
|
||||
|
||||
---
|
||||
|
||||
## 内部鉴权协议
|
||||
|
||||
> `/getScenes`、`/proxy`、`/api/session/history` 均启用此鉴权。
|
||||
> `/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 编码的用户显示名 |
|
||||
| `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 |
|
||||
| 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
|
||||
signature = HMAC-SHA256(INTERNAL_SERVICE_SECRET, message) → hex 编码
|
||||
```
|
||||
|
||||
**Python 接收方验证逻辑(`security/internal_auth.py`):**
|
||||
**Python 接收方验证逻辑:**
|
||||
|
||||
1. `INTERNAL_SERVICE_SECRET` 未配置 → 直接放行(开发环境兼容)
|
||||
2. 校验 4 个必要字段是否存在,`X-Internal-Service` 必须为 `java-gateway`
|
||||
3. 时间窗口:`abs(now_ms - timestamp) ≤ 5分钟`(防重放)
|
||||
1. `INTERNAL_SERVICE_SECRET` 未配置 → **直接放行**(开发环境兼容)
|
||||
2. 校验 `X-Internal-Service` 必须为 `java-gateway`,且 4 个必要 Header 非空
|
||||
3. 时间窗口校验:`|当前时间 - timestamp| ≤ 5 分钟`(防重放)
|
||||
4. 重新计算 HMAC,用 `hmac.compare_digest` 常量时间比较(防时序攻击)
|
||||
|
||||
---
|
||||
|
||||
## 通用错误结构
|
||||
|
||||
**内部接口(getScenes / proxy / session 系列)** 返回火山引擎风格:
|
||||
**内部接口(getScenes / proxy / session/history)** 返回火山引擎风格:
|
||||
|
||||
```json
|
||||
{
|
||||
"ResponseMetadata": {
|
||||
"Action": "xxx",
|
||||
"Action": "操作名",
|
||||
"Error": {
|
||||
"Code": "InvalidParameter",
|
||||
"Message": "SceneID 不能为空"
|
||||
"Code": -1,
|
||||
"Message": "错误描述"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**chat_callback** 返回 SSE 格式错误:
|
||||
**chat_callback** 返回 JSON 格式错误(非 SSE):
|
||||
|
||||
```
|
||||
data: {"error":{"code":"BadRequest","message":"messages 不能为空"}}
|
||||
|
||||
data: [DONE]
|
||||
```json
|
||||
{
|
||||
"Error": {
|
||||
"Code": "BadRequest",
|
||||
"Message": "messages 不能为空"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@ -499,10 +484,11 @@ 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`) |
|
||||
|--------|------|------|
|
||||
| `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`。
|
||||
|
||||
@ -128,9 +128,9 @@ LOCAL_LLM_API_KEY=你的Ark API Key
|
||||
LOCAL_LLM_MODEL=你的Ark端点ID
|
||||
|
||||
# 关键:填服务器公网 IP 或域名
|
||||
CUSTOM_LLM_URL=http://你的公网IP:3001/v1/api/chat_callback
|
||||
CUSTOM_LLM_URL=http://你的公网IP:3001/v1/chat_callback
|
||||
# 有域名 + HTTPS 则改为:
|
||||
# CUSTOM_LLM_URL=https://your-staging-domain.example.com/v1/api/chat_callback
|
||||
# CUSTOM_LLM_URL=https://your-staging-domain.example.com/v1/chat_callback
|
||||
```
|
||||
|
||||
> 火山引擎 RTC 平台需要能主动回调 `CUSTOM_LLM_URL`,所以这里必须填**公网地址**,localhost 无效。
|
||||
@ -238,14 +238,14 @@ docker run -d --env-file /secure/prod.env -p 3001:3001 aigc-backend:${IMAGE_TAG}
|
||||
| 要求 | 原因 |
|
||||
|------|------|
|
||||
| idle timeout ≥ 75s | 容器 `--timeout-keep-alive 75`,LB 超时需更长 |
|
||||
| 关闭响应缓冲 | `/api/chat_callback` 是 SSE 流式响应,Nginx 需设 `proxy_buffering off` |
|
||||
| 关闭响应缓冲 | `/v1/chat_callback` 是 SSE 流式响应,Nginx 需设 `proxy_buffering off` |
|
||||
| TLS 在 LB 层终止 | 容器内不处理 HTTPS |
|
||||
| 健康检查路径 `GET /health` | 返回 `{"status":"ok"}`,HTTP 200 |
|
||||
| sticky session(如需水平扩展)| 会话存储在内存中,同一客户端需路由到同一容器 |
|
||||
|
||||
Nginx 反向代理最小配置片段:
|
||||
```nginx
|
||||
location /v1/api/chat_callback {
|
||||
location /v1/chat_callback {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
- 根据 `backend/.env` 与 `backend/config/custom_scene.py` 构建 `Custom` 场景配置
|
||||
- 代理调用火山 RTC OpenAPI
|
||||
- 在同一个 FastAPI 进程里提供本地 `CustomLLM` 回调接口 `/api/chat_callback`
|
||||
- 在同一个 FastAPI 进程里提供本地 `CustomLLM` 回调接口 `/chat_callback`
|
||||
|
||||
## 环境要求
|
||||
|
||||
@ -68,7 +68,7 @@ cp .env.example .env
|
||||
当前 `backend` 自己对外提供回调接口,火山 RTC 会回调 `CUSTOM_LLM_URL` 指定的地址:
|
||||
|
||||
```dotenv
|
||||
CUSTOM_LLM_URL=http://127.0.0.1:3001/api/chat_callback
|
||||
CUSTOM_LLM_URL=http://127.0.0.1:3001/v1/chat_callback
|
||||
CUSTOM_LLM_API_KEY=your-callback-token
|
||||
```
|
||||
|
||||
@ -78,14 +78,14 @@ CUSTOM_LLM_API_KEY=your-callback-token
|
||||
2. 把 `CUSTOM_LLM_URL` 改成后端服务的公网 HTTPS 地址,例如:
|
||||
|
||||
```dotenv
|
||||
CUSTOM_LLM_URL=https://api.yourdomain.com/v1/api/chat_callback
|
||||
CUSTOM_LLM_URL=https://api.yourdomain.com/v1/chat_callback
|
||||
```
|
||||
|
||||
`CUSTOM_LLM_API_KEY` 是火山调用你这个本地回调接口时带上的 Bearer Token;如果你不需要这层鉴权,可以留空。
|
||||
|
||||
### 当前 backend 内置的本地 LLM 回调配置
|
||||
|
||||
`/api/chat_callback` 内部会通过 OpenAI SDK 调用方舟,因此还需要:
|
||||
`/chat_callback` 内部会通过 OpenAI SDK 调用方舟,因此还需要:
|
||||
|
||||
```dotenv
|
||||
LOCAL_LLM_API_KEY=your-ark-api-key
|
||||
@ -153,7 +153,7 @@ RAG_CONTEXT_FILE=
|
||||
2. 环境变量 `RTC_OPENAPI_VERSION`
|
||||
3. 默认值 `2025-06-01`
|
||||
|
||||
### `POST /api/chat_callback`
|
||||
### `POST /chat_callback`
|
||||
|
||||
这是当前 backend 内置的 `CustomLLM` 回调接口,也是你配置给火山 `LLMConfig.Url` 的目标地址。
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""
|
||||
POST /api/chat_callback — 自定义 LLM 回调(SSE 流式响应)
|
||||
POST /chat_callback — 自定义 LLM 回调(SSE 流式响应)
|
||||
"""
|
||||
|
||||
import json
|
||||
@ -17,7 +17,7 @@ router = APIRouter(tags=["LLM 回调"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/chat_callback",
|
||||
"/chat_callback",
|
||||
summary="自定义 LLM 回调(SSE 流式)",
|
||||
description=(
|
||||
"由**火山引擎 RTC 平台**在用户发言后自动回调,返回 OpenAI 兼容格式的 SSE 流。\n\n"
|
||||
@ -120,7 +120,7 @@ async def chat_callback(request: Request, body: ChatCallbackRequest):
|
||||
raise
|
||||
except Exception as exc:
|
||||
has_error = True
|
||||
print(f"❌ /api/chat_callback 流式输出失败: {exc}")
|
||||
print(f"❌ /chat_callback 流式输出失败: {exc}")
|
||||
|
||||
if has_error:
|
||||
print("⚠️ 已提前结束当前 SSE 流")
|
||||
|
||||
@ -32,10 +32,8 @@ async def debug_chat(request: DebugChatRequest):
|
||||
current_messages.append({"role": "user", "content": request.question})
|
||||
|
||||
start_time = time.time()
|
||||
rag_context = await rag_service.retrieve(request.question)
|
||||
stream_iterator = local_llm_service.chat_stream(
|
||||
history_messages=current_messages,
|
||||
rag_context=rag_context,
|
||||
)
|
||||
|
||||
def generate_text():
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
"""
|
||||
POST /api/session/history — 存储历史对话上下文(由 java-mock 内部调用)
|
||||
POST /session/history — 存储历史对话上下文(由 java-mock 内部调用)
|
||||
"""
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from schemas.responses import AuthFailResponse, SessionHistoryResponse
|
||||
from security.internal_auth import verify_internal_request
|
||||
from services.session_store import save_room_history
|
||||
|
||||
@ -22,16 +23,17 @@ class SetHistoryRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/session/history",
|
||||
"/session/history",
|
||||
summary="写入房间历史上下文",
|
||||
description=(
|
||||
"在 `StartVoiceChat` 之前调用,将历史对话注入该房间的上下文缓存。\n\n"
|
||||
"后续 `/api/chat_callback` 调用时会自动将缓存的历史 prepend 到每次 LLM 请求的 messages 前,"
|
||||
"后续 `/chat_callback` 调用时会自动将缓存的历史 prepend 到每次 LLM 请求的 messages 前,"
|
||||
"实现多轮对话的上下文延续。\n\n"
|
||||
"**鉴权**:需附加内部服务签名 Header(由 java-mock 自动添加)。"
|
||||
),
|
||||
response_model=SessionHistoryResponse,
|
||||
responses={
|
||||
401: {"description": "内部签名校验失败"},
|
||||
401: {"description": "内部签名校验失败", "model": AuthFailResponse},
|
||||
},
|
||||
)
|
||||
async def set_history(request: Request, body: SetHistoryRequest):
|
||||
|
||||
@ -8,6 +8,7 @@ from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from config.custom_scene import get_rtc_openapi_version
|
||||
from schemas.responses import AuthFailResponse, ProxyResponse
|
||||
from security.internal_auth import verify_internal_request
|
||||
from security.signer import Signer
|
||||
from services.scene_service import Scenes, prepare_scene_runtime
|
||||
@ -32,8 +33,9 @@ class ProxyRequest(BaseModel):
|
||||
"- `Action=StopVoiceChat`:停止对话并清除该房间的历史上下文缓存。\n\n"
|
||||
"**鉴权**:需附加内部服务签名 Header(由 java-mock 自动添加)。"
|
||||
),
|
||||
response_model=ProxyResponse,
|
||||
responses={
|
||||
401: {"description": "内部签名校验失败"},
|
||||
401: {"description": "内部签名校验失败", "model": AuthFailResponse},
|
||||
400: {"description": "参数缺失或场景配置不存在"},
|
||||
},
|
||||
)
|
||||
|
||||
@ -5,6 +5,7 @@ POST /getScenes — 场景列表
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from schemas.responses import AuthFailResponse, GetScenesResponse
|
||||
from security.internal_auth import verify_internal_request
|
||||
from services.scene_service import Scenes, prepare_scene_runtime
|
||||
from services.session_store import save_session
|
||||
@ -21,8 +22,9 @@ router = APIRouter(tags=["场景"])
|
||||
"供后续 `/proxy?Action=StartVoiceChat` 自动取用,无需客户端传递。\n\n"
|
||||
"**鉴权**:需附加内部服务签名 Header(由 java-mock 自动添加)。"
|
||||
),
|
||||
response_model=GetScenesResponse,
|
||||
responses={
|
||||
401: {"description": "内部签名校验失败"},
|
||||
401: {"description": "内部签名校验失败", "model": AuthFailResponse},
|
||||
},
|
||||
)
|
||||
async def get_scenes(request: Request):
|
||||
|
||||
93
backend/schemas/responses.py
Normal file
93
backend/schemas/responses.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""
|
||||
API 响应模型 — 用于 Swagger 文档展示响应体结构
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ─── 通用 ────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ErrorDetail(BaseModel):
|
||||
Code: int | str = Field(..., description="错误码", examples=[-1])
|
||||
Message: str = Field(..., description="错误描述", examples=["参数缺失"])
|
||||
|
||||
|
||||
class ResponseMetadata(BaseModel):
|
||||
Action: str = Field(..., description="接口名称", examples=["getScenes"])
|
||||
Error: ErrorDetail | None = Field(default=None, description="仅失败时存在")
|
||||
|
||||
|
||||
class AuthFailResponse(BaseModel):
|
||||
"""鉴权失败"""
|
||||
code: int = Field(401, description="状态码")
|
||||
message: str = Field("鉴权失败", description="错误信息")
|
||||
|
||||
|
||||
# ─── getScenes ───────────────────────────────────────────
|
||||
|
||||
|
||||
class SceneInfo(BaseModel):
|
||||
id: str = Field(..., description="场景唯一标识,后续接口的 SceneID 取此值", examples=["Custom"])
|
||||
name: str = Field(..., description="场景显示名称", examples=["小块"])
|
||||
icon: str = Field(..., description="场景头像 URL")
|
||||
botName: str | None = Field(None, description="AI Bot 在 RTC 房间中的 UserId")
|
||||
isInterruptMode: bool = Field(..., description="是否开启用户打断模式")
|
||||
isVision: bool | None = Field(None, description="是否开启视觉理解能力")
|
||||
isScreenMode: bool = Field(False, description="是否为屏幕共享模式")
|
||||
isAvatarScene: bool | None = Field(None, description="是否为数字人场景")
|
||||
avatarBgUrl: str | None = Field(None, description="数字人背景图 URL")
|
||||
|
||||
|
||||
class RTCInfo(BaseModel):
|
||||
AppId: str = Field(..., description="火山引擎 RTC AppId")
|
||||
RoomId: str = Field(..., description="本次生成的房间 ID(UUID)")
|
||||
UserId: str = Field(..., description="本次生成的用户 ID(UUID)")
|
||||
Token: str = Field(..., description="RTC 入房 Token,24 小时有效")
|
||||
TaskId: str = Field(..., description="语音任务 ID,StopVoiceChat 时需要")
|
||||
|
||||
|
||||
class SceneItem(BaseModel):
|
||||
scene: SceneInfo
|
||||
rtc: RTCInfo
|
||||
|
||||
|
||||
class GetScenesResult(BaseModel):
|
||||
scenes: list[SceneItem] = Field(..., description="场景列表")
|
||||
|
||||
|
||||
class GetScenesResponse(BaseModel):
|
||||
"""获取场景列表成功响应"""
|
||||
ResponseMetadata: ResponseMetadata
|
||||
Result: GetScenesResult
|
||||
|
||||
|
||||
# ─── proxy ───────────────────────────────────────────────
|
||||
|
||||
|
||||
class ProxyResultData(BaseModel):
|
||||
Message: str = Field(..., description="结果信息", examples=["success"])
|
||||
|
||||
|
||||
class ProxyResponseMetadata(BaseModel):
|
||||
RequestId: str | None = Field(None, description="火山引擎请求 ID")
|
||||
Action: str = Field(..., description="操作类型", examples=["StartVoiceChat"])
|
||||
Version: str = Field(..., description="OpenAPI 版本", examples=["2025-06-01"])
|
||||
Service: str = Field("rtc", description="服务名")
|
||||
Error: ErrorDetail | None = Field(default=None, description="仅失败时存在")
|
||||
|
||||
|
||||
class ProxyResponse(BaseModel):
|
||||
"""代理接口成功响应(透传火山引擎返回)"""
|
||||
ResponseMetadata: ProxyResponseMetadata
|
||||
Result: ProxyResultData | None = Field(None, description="成功时的结果数据")
|
||||
|
||||
|
||||
# ─── session/history ─────────────────────────────────────
|
||||
|
||||
|
||||
class SessionHistoryResponse(BaseModel):
|
||||
"""写入历史上下文成功响应"""
|
||||
code: int = Field(200, description="状态码", examples=[200])
|
||||
@ -20,6 +20,9 @@ def verify_internal_request(headers) -> bool:
|
||||
验证内部服务请求签名。
|
||||
未配置 INTERNAL_SERVICE_SECRET 时直接放行(开发/测试环境兼容)。
|
||||
"""
|
||||
# TODO: 开发阶段临时关闭鉴权,上线前需恢复
|
||||
return True
|
||||
|
||||
secret = os.environ.get("INTERNAL_SERVICE_SECRET", "")
|
||||
if not secret:
|
||||
return True
|
||||
|
||||
@ -35,9 +35,9 @@ app = FastAPI(
|
||||
description=(
|
||||
"火山引擎 RTC AI 语音对话后端服务。\n\n"
|
||||
"## 鉴权说明\n"
|
||||
"- **内部接口**(`/getScenes` `/proxy` `/api/session/history`):由 java-mock 网关调用,"
|
||||
"- **内部接口**(`/getScenes` `/proxy` `/session/history`):由 java-mock 网关调用,"
|
||||
"需附加 `X-Internal-Signature` HMAC 签名 Header。\n"
|
||||
"- **LLM 回调**(`/api/chat_callback`):由火山引擎 RTC 平台回调,"
|
||||
"- **LLM 回调**(`/chat_callback`):由火山引擎 RTC 平台回调,"
|
||||
"需在 `Authorization: Bearer <key>` 中携带 `CUSTOM_LLM_API_KEY`。\n"
|
||||
"- **调试接口**(`/debug/*`):无鉴权,仅用于本地开发。"
|
||||
),
|
||||
|
||||
@ -27,6 +27,7 @@ def load_scenes() -> dict:
|
||||
scenes = {
|
||||
CUSTOM_SCENE_ID: build_custom_scene_from_env(),
|
||||
}
|
||||
if SCENES_DIR.is_dir():
|
||||
for p in sorted(SCENES_DIR.glob("*.json")):
|
||||
if p.stem == CUSTOM_SCENE_ID:
|
||||
continue
|
||||
|
||||
@ -11,17 +11,24 @@ import re
|
||||
import httpx
|
||||
|
||||
from tools.registry import tool_registry
|
||||
from utils.env import env_int, env_str
|
||||
|
||||
# ── 公共默认值(所有接口共享) ─────────────────────────────
|
||||
|
||||
DEFAULT_BASE_URL = "https://hskuaikuai.com:9000/test" # TODO: 替换为实际服务地址
|
||||
|
||||
DEFAULT_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"token": "265a068eb7af455ca97f1a5063561a5d", # TODO: 后续由前端传入
|
||||
}
|
||||
def _get_base_url() -> str:
|
||||
return env_str("API_TOOLS_BASE_URL", "https://hskuaikuai.com:9000/test")
|
||||
|
||||
DEFAULT_TIMEOUT = 10 # 秒
|
||||
|
||||
def _get_default_headers() -> dict[str, str]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
token = env_str("API_TOOLS_TOKEN")
|
||||
if token:
|
||||
headers["token"] = token
|
||||
return headers
|
||||
|
||||
|
||||
DEFAULT_TIMEOUT = env_int("API_TOOLS_TIMEOUT", 10)
|
||||
|
||||
|
||||
# ── API 端点声明 ─────────────────────────────────────────
|
||||
@ -139,7 +146,8 @@ _PATH_PARAM_RE = re.compile(r"\{(\w+)\}")
|
||||
|
||||
def _call_api(endpoint: dict, **kwargs) -> str:
|
||||
method = endpoint["method"].upper()
|
||||
headers = {**DEFAULT_HEADERS, **endpoint.get("headers", {})}
|
||||
base_url = _get_base_url()
|
||||
headers = {**_get_default_headers(), **endpoint.get("headers", {})}
|
||||
|
||||
# "全部"部门:批量查询所有部门,合并摘要后返回
|
||||
if endpoint["name"] == "get_checkin_app_dept" and kwargs.get("deptName") == "全部":
|
||||
@ -147,7 +155,7 @@ def _call_api(endpoint: dict, **kwargs) -> str:
|
||||
combined = {}
|
||||
for dept in all_depts:
|
||||
try:
|
||||
url = DEFAULT_BASE_URL.rstrip("/") + endpoint["path"]
|
||||
url = base_url.rstrip("/") + endpoint["path"]
|
||||
resp = httpx.request(method, url, headers=headers, json={"deptName": dept}, timeout=DEFAULT_TIMEOUT)
|
||||
resp.raise_for_status()
|
||||
summary = _summarize_checkin_dept(resp.text)
|
||||
@ -158,7 +166,7 @@ def _call_api(endpoint: dict, **kwargs) -> str:
|
||||
print(f"全部部门摘要: {result[:300]}...")
|
||||
return result
|
||||
|
||||
url = DEFAULT_BASE_URL.rstrip("/") + endpoint["path"]
|
||||
url = base_url.rstrip("/") + endpoint["path"]
|
||||
# 1) 路径参数替换:{user_id} → kwargs["user_id"]
|
||||
path_params = set(_PATH_PARAM_RE.findall(url))
|
||||
for p in path_params:
|
||||
|
||||
@ -326,7 +326,7 @@ POST /api/ai/session/history
|
||||
|
||||
**请求体(JSON):**
|
||||
|
||||
转发到 Python `/api/session/history`,具体字段由 Python 接口定义,典型示例:
|
||||
转发到 Python `/session/history`,具体字段由 Python 接口定义,典型示例:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@ -575,5 +575,51 @@
|
||||
"createdAt": "2026-04-02T09:31:39.293Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "29119c56-6e65-42a8-81c9-bb4574895940",
|
||||
"userId": "user-admin-001",
|
||||
"sceneId": "Custom",
|
||||
"roomId": "de26efa0-6d90-4b37-96ce-741af9b23bd5",
|
||||
"startedAt": "2026-04-03T07:25:21.765Z",
|
||||
"endedAt": "2026-04-03T07:25:58.942Z",
|
||||
"createdAt": "2026-04-03T07:26:37.884Z",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "你好,我是小块,有什么需要帮忙的吗?",
|
||||
"createdAt": "2026-04-03T07:25:21.765Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "啊! 你返回的应该是这种这才是啥意思?我看你返回的你做包做做包了一层。",
|
||||
"createdAt": "2026-04-03T07:25:28.458Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "这个。",
|
||||
"createdAt": "2026-04-03T07:25:30.538Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "这个才是他要的。",
|
||||
"createdAt": "2026-04-03T07:25:36.686Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你那个逮他,你那个就是有一个这个东西是吧。对,因为他拿的不是这东西,默认返回的。",
|
||||
"createdAt": "2026-04-03T07:25:46.945Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "取消你要不就给这,要不就全能改,要不就后来取消。",
|
||||
"createdAt": "2026-04-03T07:25:54.080Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你在干嘛?",
|
||||
"createdAt": "2026-04-03T07:25:58.942Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -49,7 +49,7 @@ router.post('/proxy', authMiddleware, async (req, res) => {
|
||||
|
||||
// POST /api/ai/session/history — 存储历史对话上下文到 Python(start 前调用)
|
||||
router.post('/session/history', authMiddleware, async (req, res) => {
|
||||
await forwardToPython('/api/session/history', '', req.body, req.user.userId, res, req.user);
|
||||
await forwardToPython('/session/history', '', req.body, req.user.userId, res, req.user);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
│ ↑
|
||||
↓ 火山RTC平台
|
||||
┌──────────┐ 直接调用
|
||||
│ MySQL/PG │ /api/chat_callback
|
||||
│ MySQL/PG │ /chat_callback
|
||||
│ 对话历史 │
|
||||
└──────────┘
|
||||
```
|
||||
@ -70,7 +70,7 @@
|
||||
[4] 语音对话进行中
|
||||
┌─ 语音数据: Frontend ←→ 火山 RTC 服务器 (WebRTC, 不走 HTTP) ─┐
|
||||
│ 字幕/状态: 火山 RTC → Frontend (RTC Binary Message, TLV) │
|
||||
│ LLM 回调: 火山 RTC → Python /api/chat_callback (SSE) │
|
||||
│ LLM 回调: 火山 RTC → Python /chat_callback (SSE) │
|
||||
└─────── 这三条通道都不经过 Java ──────────────────────────────┘
|
||||
|
||||
[5] 结束通话
|
||||
@ -89,7 +89,7 @@
|
||||
| `POST /getScenes` | 走 | 需要用户身份 |
|
||||
| `POST /proxy?Action=StartVoiceChat` | 走 | 需要用户身份 |
|
||||
| `POST /proxy?Action=StopVoiceChat` | 走 | 需要用户身份 |
|
||||
| `POST /api/chat_callback` | **不走** | 火山 RTC 平台直接调用 Python |
|
||||
| `POST /chat_callback` | **不走** | 火山 RTC 平台直接调用 Python |
|
||||
| 语音音频流 | **不走** | WebRTC P2P / 媒体服务器 |
|
||||
| 字幕/状态消息 | **不走** | RTC Binary Message (TLV) |
|
||||
| 对话历史保存 | 直接到 Java | Java 自己处理,不转发 Python |
|
||||
@ -153,7 +153,7 @@ def verify_internal_auth(request):
|
||||
|
||||
| 路径 | 原因 |
|
||||
|------|------|
|
||||
| `/api/chat_callback` | 火山 RTC 平台调用,有自己的 Bearer Token 鉴权 |
|
||||
| `/chat_callback` | 火山 RTC 平台调用,有自己的 Bearer Token 鉴权 |
|
||||
| `/debug/*` | 开发调试用,生产环境应在网络层禁止访问 |
|
||||
|
||||
### 3.5 本地开发兼容
|
||||
@ -249,7 +249,7 @@ def _key(request: Request) -> str:
|
||||
|
||||
### 4.3 身份关联:chat_callback 场景
|
||||
|
||||
`/api/chat_callback` 由火山 RTC 平台调用,不经过 Java,不携带用户身份。但可通过 RoomId 关联用户:
|
||||
`/chat_callback` 由火山 RTC 平台调用,不经过 Java,不携带用户身份。但可通过 RoomId 关联用户:
|
||||
|
||||
```
|
||||
getScenes 阶段:
|
||||
@ -513,7 +513,7 @@ window.addEventListener('beforeunload', this._beforeUnloadHandler);
|
||||
HMAC 签名验证中间件:
|
||||
- 读取 `INTERNAL_SERVICE_SECRET` 环境变量
|
||||
- 对非豁免路径验证 `X-Internal-Signature`
|
||||
- 豁免路径:`/api/chat_callback`、`/debug/*`
|
||||
- 豁免路径:`/chat_callback`、`/debug/*`
|
||||
- 密钥为空时跳过验证(本地开发兼容)
|
||||
|
||||
#### 6.1.3 修改 `backend/services/session_store.py`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user