From 6daaae00d35663059a7f3cb455cdee681d2586f3 Mon Sep 17 00:00:00 2001 From: lengbone <107662693+lengbone@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:00:29 +0800 Subject: [PATCH] refactor: update API endpoints to use consistent path structure, changing `/api/chat_callback` to `/chat_callback` and adjusting related documentation and configurations accordingly. --- backend/.dockerignore | 1 + backend/.env.example | 15 +- backend/.env.staging | 2 +- backend/API.md | 680 ++++++++++++++--------------- backend/DEPLOYMENT.md | 8 +- backend/README.md | 10 +- backend/routes/v1/chat_callback.py | 6 +- backend/routes/v1/debug.py | 2 - backend/routes/v1/history.py | 10 +- backend/routes/v1/proxy.py | 4 +- backend/routes/v1/scenes.py | 4 +- backend/schemas/responses.py | 93 ++++ backend/security/internal_auth.py | 3 + backend/server.py | 4 +- backend/services/scene_service.py | 11 +- backend/tools/builtin/api_tools.py | 26 +- java-mock/API-FULL.md | 2 +- java-mock/data/conversations.json | 46 ++ java-mock/routes/aiProxyRoutes.js | 2 +- plan/integration-plan.md | 12 +- 20 files changed, 544 insertions(+), 397 deletions(-) create mode 100644 backend/schemas/responses.py diff --git a/backend/.dockerignore b/backend/.dockerignore index 7256230..5fb594a 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -9,4 +9,5 @@ __pycache__/ .vscode/ .DS_Store *.md +!prompts/*.md *.log diff --git a/backend/.env.example b/backend/.env.example index b2ba984..44a8363 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -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 diff --git a/backend/.env.staging b/backend/.env.staging index 014396d..e83bec5 100644 --- a/backend/.env.staging +++ b/backend/.env.staging @@ -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 diff --git a/backend/API.md b/backend/API.md index f0a1401..ac77993 100644 --- a/backend/API.md +++ b/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=&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= +POST /v1/proxy?Action={Action}&Version={Version} ``` -**鉴权:** `Authorization: Bearer `(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 ` + +**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= +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`。 diff --git a/backend/DEPLOYMENT.md b/backend/DEPLOYMENT.md index aca6abf..182a791 100644 --- a/backend/DEPLOYMENT.md +++ b/backend/DEPLOYMENT.md @@ -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; diff --git a/backend/README.md b/backend/README.md index e0798d8..f09b675 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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` 的目标地址。 diff --git a/backend/routes/v1/chat_callback.py b/backend/routes/v1/chat_callback.py index 8ca3edd..60631ce 100644 --- a/backend/routes/v1/chat_callback.py +++ b/backend/routes/v1/chat_callback.py @@ -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 流") diff --git a/backend/routes/v1/debug.py b/backend/routes/v1/debug.py index 975311a..89b6842 100644 --- a/backend/routes/v1/debug.py +++ b/backend/routes/v1/debug.py @@ -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(): diff --git a/backend/routes/v1/history.py b/backend/routes/v1/history.py index a79ab3d..c596b00 100644 --- a/backend/routes/v1/history.py +++ b/backend/routes/v1/history.py @@ -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): diff --git a/backend/routes/v1/proxy.py b/backend/routes/v1/proxy.py index 54ae545..6355776 100644 --- a/backend/routes/v1/proxy.py +++ b/backend/routes/v1/proxy.py @@ -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": "参数缺失或场景配置不存在"}, }, ) diff --git a/backend/routes/v1/scenes.py b/backend/routes/v1/scenes.py index c7e4c42..d8b9e5b 100644 --- a/backend/routes/v1/scenes.py +++ b/backend/routes/v1/scenes.py @@ -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): diff --git a/backend/schemas/responses.py b/backend/schemas/responses.py new file mode 100644 index 0000000..88c6f7a --- /dev/null +++ b/backend/schemas/responses.py @@ -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]) diff --git a/backend/security/internal_auth.py b/backend/security/internal_auth.py index e73e5d8..679c3cd 100644 --- a/backend/security/internal_auth.py +++ b/backend/security/internal_auth.py @@ -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 diff --git a/backend/server.py b/backend/server.py index d5f5256..6ecf2a3 100644 --- a/backend/server.py +++ b/backend/server.py @@ -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 ` 中携带 `CUSTOM_LLM_API_KEY`。\n" "- **调试接口**(`/debug/*`):无鉴权,仅用于本地开发。" ), diff --git a/backend/services/scene_service.py b/backend/services/scene_service.py index 2273bcf..12ca6c7 100644 --- a/backend/services/scene_service.py +++ b/backend/services/scene_service.py @@ -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 diff --git a/backend/tools/builtin/api_tools.py b/backend/tools/builtin/api_tools.py index 0486fa9..6841518 100644 --- a/backend/tools/builtin/api_tools.py +++ b/backend/tools/builtin/api_tools.py @@ -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: diff --git a/java-mock/API-FULL.md b/java-mock/API-FULL.md index fabfba6..04e1d0e 100644 --- a/java-mock/API-FULL.md +++ b/java-mock/API-FULL.md @@ -326,7 +326,7 @@ POST /api/ai/session/history **请求体(JSON):** -转发到 Python `/api/session/history`,具体字段由 Python 接口定义,典型示例: +转发到 Python `/session/history`,具体字段由 Python 接口定义,典型示例: ```json { diff --git a/java-mock/data/conversations.json b/java-mock/data/conversations.json index 8583d90..0306ce2 100644 --- a/java-mock/data/conversations.json +++ b/java-mock/data/conversations.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" + } + ] } ] \ No newline at end of file diff --git a/java-mock/routes/aiProxyRoutes.js b/java-mock/routes/aiProxyRoutes.js index a9c99f7..54f38ca 100644 --- a/java-mock/routes/aiProxyRoutes.js +++ b/java-mock/routes/aiProxyRoutes.js @@ -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; diff --git a/plan/integration-plan.md b/plan/integration-plan.md index 233bd4b..4a8efff 100644 --- a/plan/integration-plan.md +++ b/plan/integration-plan.md @@ -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`