rtc-voice-chat/backend/routes/v1/proxy.py
2026-04-02 20:15:15 +08:00

132 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
POST /proxy — RTC OpenAPI 代理(含请求签名)
"""
import httpx
from fastapi import APIRouter, Query, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from config.custom_scene import get_rtc_openapi_version
from security.internal_auth import verify_internal_request
from security.signer import Signer
from services.scene_service import Scenes, prepare_scene_runtime
from services.session_store import clear_room_history, get_room_history, load_session
from utils.responses import error_response
from utils.validation import assert_scene_value, assert_value
router = APIRouter(tags=["RTC 代理"])
class ProxyRequest(BaseModel):
SceneID: str = Field(..., description="场景 ID从 getScenes 返回的 scene.id 获取)")
@router.post(
"/proxy",
summary="开始 / 停止语音对话",
description=(
"带 SigV4 签名转发到火山引擎 RTC OpenAPI。\n\n"
"- `Action=StartVoiceChat`:从 Session 取回 getScenes 分配的 RoomId/TaskId"
"自动注入后启动对话。\n"
"- `Action=StopVoiceChat`:停止对话并清除该房间的历史上下文缓存。\n\n"
"**鉴权**:需附加内部服务签名 Header由 java-mock 自动添加)。"
),
responses={
401: {"description": "内部签名校验失败"},
400: {"description": "参数缺失或场景配置不存在"},
},
)
async def proxy(
request: Request,
body: ProxyRequest,
action: str = Query(..., alias="Action", description="操作类型:`StartVoiceChat` 或 `StopVoiceChat`"),
version: str | None = Query(None, alias="Version", description="火山引擎 OpenAPI 版本,不传时取配置文件默认值"),
):
if not verify_internal_request(request.headers):
return JSONResponse({"code": 401, "message": "鉴权失败"}, status_code=401)
version = version or get_rtc_openapi_version()
scene_id = body.SceneID
try:
assert_value(action, "Action 不能为空")
assert_value(version, "Version 不能为空")
assert_value(scene_id, "SceneID 不能为空SceneID 用于指定场景配置")
json_data = Scenes.get(scene_id)
if not json_data:
raise ValueError(f"{scene_id} 不存在,请先配置对应场景。")
_, _, voice_chat = prepare_scene_runtime(scene_id, json_data)
account_config = json_data.get("AccountConfig", {})
assert_scene_value(
scene_id, "AccountConfig.accessKeyId", account_config.get("accessKeyId")
)
assert_scene_value(
scene_id, "AccountConfig.secretKey", account_config.get("secretKey")
)
if action == "StartVoiceChat":
# 从 session 取回 getScenes 时分配的 RoomId/UserId/TaskId
sess = load_session(request, scene_id)
if sess.get("RoomId"):
voice_chat["RoomId"] = sess["RoomId"]
if sess.get("TaskId"):
voice_chat["TaskId"] = sess["TaskId"]
if sess.get("UserId"):
agent_config = voice_chat.get("AgentConfig", {})
target_user_ids = agent_config.get("TargetUserId", [])
if target_user_ids:
target_user_ids[0] = sess["UserId"]
else:
agent_config["TargetUserId"] = [sess["UserId"]]
# 将 room_id 追加到 LLM 回调 URL使 chat_callback 能关联到历史
room_id = voice_chat.get("RoomId", "")
if room_id:
llm_config = voice_chat.get("Config", {}).get("LLMConfig", {})
llm_url = llm_config.get("Url", "")
if llm_url:
sep = "&" if "?" in llm_url else "?"
llm_config["Url"] = f"{llm_url}{sep}room_id={room_id}"
req_body = voice_chat
elif action == "StopVoiceChat":
app_id = voice_chat.get("AppId", "")
sess = load_session(request, scene_id)
room_id = sess.get("RoomId") or voice_chat.get("RoomId", "")
task_id = sess.get("TaskId") or voice_chat.get("TaskId", "")
assert_scene_value(scene_id, "VoiceChat.AppId", app_id)
assert_scene_value(scene_id, "VoiceChat.RoomId", room_id)
assert_scene_value(scene_id, "VoiceChat.TaskId", task_id)
# 清除该房间的历史上下文
clear_room_history(room_id)
req_body = {"AppId": app_id, "RoomId": room_id, "TaskId": task_id}
else:
req_body = {}
request_data = {
"region": "cn-north-1",
"method": "POST",
"params": {"Action": action, "Version": version},
"headers": {
"Host": "rtc.volcengineapi.com",
"Content-type": "application/json",
},
"body": req_body,
}
signer = Signer(request_data, "rtc")
signer.add_authorization(account_config)
async with httpx.AsyncClient() as client:
resp = await client.post(
f"https://rtc.volcengineapi.com?Action={action}&Version={version}",
headers=request_data["headers"],
json=req_body,
)
return JSONResponse(resp.json())
except ValueError as e:
return error_response(action, str(e))
except Exception as e:
return error_response(action, str(e))