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:
lengbone 2026-04-08 11:00:29 +08:00
parent 271f7345c9
commit 6daaae00d3
20 changed files with 544 additions and 397 deletions

View File

@ -9,4 +9,5 @@ __pycache__/
.vscode/
.DS_Store
*.md
!prompts/*.md
*.log

View File

@ -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

View File

@ -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

View File

@ -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 | 本次会话分配的房间 IDUUID |
| UserId | string | 用户在 RTC 房间中的 UserId |
| Token | string | RTC 入房 Token |
| TaskId | string | 语音任务 IDStopVoiceChat 时需要 |
|------|------|------|
| `AppId` | string | 火山引擎 RTC AppId |
| `RoomId` | string | 本次生成的房间 IDUUID前端入房和后续接口都需要 |
| `UserId` | string | 本次生成的用户 IDUUID前端入房使用 |
| `Token` | string | RTC 入房 Token24 小时有效 |
| `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**
**BodyJSON**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 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`
**成功响应 200text/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](#请求)
**BodyJSON**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `room_id` | string | ✅ | RTC 房间 ID`getScenes` 返回的 `rtc.RoomId` |
| `messages` | array | ✅ | 历史消息列表,按时间正序排列 |
**`messages[*]` 字段说明:**
| 字段 | 类型 | 必填 | 可选值 | 说明 |
|------|------|------|--------|------|
| `role` | string | ✅ | `"user"` \| `"assistant"` | 消息发送方角色 |
| `content` | string | ✅ | — | 消息文本内容 |
**请求示例:**
```json
{
"room_id": "550e8400-e29b-41d4-a716-446655440000",
"messages": [
{ "role": "assistant", "content": "你好,我是小块,有什么需要帮忙的吗?" },
{ "role": "user", "content": "今天出勤情况咋样" },
{ "role": "assistant", "content": "今天全部门出勤率百分之九十五,共二十三人到岗。" }
]
}
```
### 成功响应 `200`
```json
{
"code": 200
}
```
### 失败响应
**鉴权失败 `401`**
```json
{
"code": 401,
"message": "鉴权失败"
}
```
**Body 校验失败 `422`FastAPI 自动校验):**
```json
{
"detail": [
{
"type": "missing",
"loc": ["body", "room_id"],
"msg": "Field required"
}
]
}
```
> **注意:**
> - 每次调用会**覆盖**该 `room_id` 下已有的历史,不是追加
> - `StopVoiceChat` 时会自动清除该房间的历史缓存
> - 如果不需要上下文延续(新对话),可以跳过此接口
---
## 四、自定义 LLM 回调SSE
> 此接口由**火山引擎 RTC 平台**自动回调,不经过 java-mock。
```
POST /v1/chat_callback?room_id={room_id}
```
**鉴权:** `Authorization: Bearer <CUSTOM_LLM_API_KEY>`
**Query 参数:**
| 参数 | 必填 | 说明 |
|------|------|------|
| `room_id` | ❌ | 房间 ID`StartVoiceChat` 自动追加到回调 URL 中 |
**BodyJSON**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `messages` | array | ✅ | 对话消息列表,最后一条必须是 `user` 角色 |
| `temperature` | float | ❌ | 采样温度 |
| `max_tokens` | int | ❌ | 最大生成 token 数 |
| `top_p` | float | ❌ | Top-P 采样 |
**成功响应 `200``text/event-stream`**
```
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"今"}}]}
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"天"}}]}
data: [DONE]
```
**失败响应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": "今天办公室有多少人打卡?"
}
```
**成功响应 200text/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`

View File

@ -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;

View File

@ -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` 的目标地址。

View File

@ -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 流")

View File

@ -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():

View File

@ -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):

View File

@ -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": "参数缺失或场景配置不存在"},
},
)

View File

@ -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):

View 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="本次生成的房间 IDUUID")
UserId: str = Field(..., description="本次生成的用户 IDUUID")
Token: str = Field(..., description="RTC 入房 Token24 小时有效")
TaskId: str = Field(..., description="语音任务 IDStopVoiceChat 时需要")
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])

View File

@ -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

View File

@ -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/*`):无鉴权,仅用于本地开发。"
),

View File

@ -27,11 +27,12 @@ def load_scenes() -> dict:
scenes = {
CUSTOM_SCENE_ID: build_custom_scene_from_env(),
}
for p in sorted(SCENES_DIR.glob("*.json")):
if p.stem == CUSTOM_SCENE_ID:
continue
with open(p, encoding="utf-8") as f:
scenes[p.stem] = json.load(f)
if SCENES_DIR.is_dir():
for p in sorted(SCENES_DIR.glob("*.json")):
if p.stem == CUSTOM_SCENE_ID:
continue
with open(p, encoding="utf-8") as f:
scenes[p.stem] = json.load(f)
return scenes

View File

@ -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:

View File

@ -326,7 +326,7 @@ POST /api/ai/session/history
**请求体JSON**
转发到 Python `/api/session/history`,具体字段由 Python 接口定义,典型示例:
转发到 Python `/session/history`,具体字段由 Python 接口定义,典型示例:
```json
{

View File

@ -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"
}
]
}
]

View File

@ -49,7 +49,7 @@ router.post('/proxy', authMiddleware, async (req, res) => {
// POST /api/ai/session/history — 存储历史对话上下文到 Pythonstart 前调用)
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;

View File

@ -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`