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