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