mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-21 22:53:56 +08:00
175 lines
6.7 KiB
Python
175 lines
6.7 KiB
Python
import json
|
|
import copy
|
|
from aiohttp import web
|
|
from config.logger import setup_logging
|
|
from core.api.base_handler import BaseHandler
|
|
from core.utils.util import get_vision_url, is_valid_image_file
|
|
from core.utils.vllm import create_instance
|
|
from config.config_loader import get_private_config_from_api
|
|
from core.utils.auth import AuthToken
|
|
import base64
|
|
from typing import Tuple, Optional
|
|
from plugins_func.register import Action
|
|
|
|
TAG = __name__
|
|
|
|
# 设置最大文件大小为5MB
|
|
MAX_FILE_SIZE = 5 * 1024 * 1024
|
|
|
|
|
|
class VisionHandler(BaseHandler):
|
|
def __init__(self, config: dict):
|
|
super().__init__(config)
|
|
# 初始化认证工具
|
|
self.auth = AuthToken(config["server"]["auth_key"])
|
|
|
|
def _create_error_response(self, message: str) -> dict:
|
|
"""创建统一的错误响应格式"""
|
|
return {"success": False, "message": message}
|
|
|
|
def _verify_auth_token(self, request) -> Tuple[bool, Optional[str]]:
|
|
"""验证认证token"""
|
|
auth_header = request.headers.get("Authorization", "")
|
|
if not auth_header.startswith("Bearer "):
|
|
return False, None
|
|
|
|
token = auth_header[7:] # 移除"Bearer "前缀
|
|
return self.auth.verify_token(token)
|
|
|
|
async def handle_post(self, request):
|
|
"""处理 MCP Vision POST 请求"""
|
|
response = None # 初始化response变量
|
|
try:
|
|
# 验证token
|
|
is_valid, token_device_id = self._verify_auth_token(request)
|
|
if not is_valid:
|
|
response = web.Response(
|
|
text=json.dumps(
|
|
self._create_error_response("无效的认证token或token已过期")
|
|
),
|
|
content_type="application/json",
|
|
status=401,
|
|
)
|
|
return response
|
|
|
|
# 获取请求头信息
|
|
device_id = request.headers.get("Device-Id", "")
|
|
client_id = request.headers.get("Client-Id", "")
|
|
if device_id != token_device_id:
|
|
raise ValueError("设备ID与token不匹配")
|
|
# 解析multipart/form-data请求
|
|
reader = await request.multipart()
|
|
|
|
# 读取question字段
|
|
question_field = await reader.next()
|
|
if question_field is None:
|
|
raise ValueError("缺少问题字段")
|
|
question = await question_field.text()
|
|
self.logger.bind(tag=TAG).debug(f"Question: {question}")
|
|
|
|
# 读取图片文件
|
|
image_field = await reader.next()
|
|
if image_field is None:
|
|
raise ValueError("缺少图片文件")
|
|
|
|
# 读取图片数据
|
|
image_data = await image_field.read()
|
|
if not image_data:
|
|
raise ValueError("图片数据为空")
|
|
|
|
# 检查文件大小
|
|
if len(image_data) > MAX_FILE_SIZE:
|
|
raise ValueError(
|
|
f"图片大小超过限制,最大允许{MAX_FILE_SIZE/1024/1024}MB"
|
|
)
|
|
|
|
# 检查文件格式
|
|
if not is_valid_image_file(image_data):
|
|
raise ValueError(
|
|
"不支持的文件格式,请上传有效的图片文件(支持JPEG、PNG、GIF、BMP、TIFF、WEBP格式)"
|
|
)
|
|
|
|
# 将图片转换为base64编码
|
|
image_base64 = base64.b64encode(image_data).decode("utf-8")
|
|
|
|
# 如果开启了智控台,则从智控台获取模型配置
|
|
current_config = copy.deepcopy(self.config)
|
|
read_config_from_api = current_config.get("read_config_from_api", False)
|
|
if read_config_from_api:
|
|
current_config = await get_private_config_from_api(
|
|
current_config,
|
|
device_id,
|
|
client_id,
|
|
)
|
|
|
|
select_vllm_module = current_config["selected_module"].get("VLLM")
|
|
if not select_vllm_module:
|
|
raise ValueError("您还未设置默认的视觉分析模块")
|
|
|
|
vllm_type = (
|
|
select_vllm_module
|
|
if "type" not in current_config["VLLM"][select_vllm_module]
|
|
else current_config["VLLM"][select_vllm_module]["type"]
|
|
)
|
|
|
|
if not vllm_type:
|
|
raise ValueError(f"无法找到VLLM模块对应的供应器{vllm_type}")
|
|
|
|
vllm = create_instance(
|
|
vllm_type, current_config["VLLM"][select_vllm_module]
|
|
)
|
|
|
|
result = vllm.response(question, image_base64)
|
|
|
|
return_json = {
|
|
"success": True,
|
|
"action": Action.RESPONSE.name,
|
|
"response": result,
|
|
}
|
|
|
|
response = web.Response(
|
|
text=json.dumps(return_json, separators=(",", ":")),
|
|
content_type="application/json",
|
|
)
|
|
except ValueError as e:
|
|
self.logger.bind(tag=TAG).error(f"MCP Vision POST请求异常: {e}")
|
|
return_json = self._create_error_response(str(e))
|
|
response = web.Response(
|
|
text=json.dumps(return_json, separators=(",", ":")),
|
|
content_type="application/json",
|
|
)
|
|
except Exception as e:
|
|
self.logger.bind(tag=TAG).error(f"MCP Vision POST请求异常: {e}")
|
|
return_json = self._create_error_response("处理请求时发生错误")
|
|
response = web.Response(
|
|
text=json.dumps(return_json, separators=(",", ":")),
|
|
content_type="application/json",
|
|
)
|
|
finally:
|
|
if response:
|
|
self._add_cors_headers(response)
|
|
return response
|
|
|
|
async def handle_get(self, request):
|
|
"""处理 MCP Vision GET 请求"""
|
|
try:
|
|
vision_explain = get_vision_url(self.config)
|
|
if vision_explain and len(vision_explain) > 0 and "null" != vision_explain:
|
|
message = (
|
|
f"MCP Vision 接口运行正常,视觉解释接口地址是:{vision_explain}"
|
|
)
|
|
else:
|
|
message = "MCP Vision 接口运行不正常,请打开data目录下的.config.yaml文件,找到【server.vision_explain】,设置好地址"
|
|
|
|
response = web.Response(text=message, content_type="text/plain")
|
|
except Exception as e:
|
|
self.logger.bind(tag=TAG).error(f"MCP Vision GET请求异常: {e}")
|
|
return_json = self._create_error_response("服务器内部错误")
|
|
response = web.Response(
|
|
text=json.dumps(return_json, separators=(",", ":")),
|
|
content_type="application/json",
|
|
)
|
|
finally:
|
|
self._add_cors_headers(response)
|
|
return response
|