132 lines
5.3 KiB
Python
132 lines
5.3 KiB
Python
"""
|
||
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))
|