From 6e7c86e159ffa8e51fdba92d47f47a036e4f346a Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 18 Dec 2025 17:37:20 +0800 Subject: [PATCH] =?UTF-8?q?update:=E4=BB=8Evision=5Furl=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=87=8C=E8=AF=BB=E5=8F=96=E5=9F=9F=E5=90=8D=E5=92=8C=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/api/base_handler.py | 10 ++- main/xiaozhi-server/core/api/ota_handler.py | 63 +++++++++------ .../xiaozhi-server/core/api/vision_handler.py | 14 +--- main/xiaozhi-server/core/http_server.py | 79 ++++++++++++------- 4 files changed, 99 insertions(+), 67 deletions(-) diff --git a/main/xiaozhi-server/core/api/base_handler.py b/main/xiaozhi-server/core/api/base_handler.py index 7330185e..db277543 100644 --- a/main/xiaozhi-server/core/api/base_handler.py +++ b/main/xiaozhi-server/core/api/base_handler.py @@ -10,7 +10,15 @@ class BaseHandler: def _add_cors_headers(self, response): """添加CORS头信息""" response.headers["Access-Control-Allow-Headers"] = ( - "client-id, content-type, device-id" + "client-id, content-type, device-id, authorization" ) response.headers["Access-Control-Allow-Credentials"] = "true" response.headers["Access-Control-Allow-Origin"] = "*" + + async def handle_options(self, request): + """处理OPTIONS请求,添加CORS头信息""" + response = web.Response(body=b"", content_type="text/plain") + self._add_cors_headers(response) + # 添加允许的方法 + response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" + return response diff --git a/main/xiaozhi-server/core/api/ota_handler.py b/main/xiaozhi-server/core/api/ota_handler.py index 1fe339c2..1e3b3bd0 100644 --- a/main/xiaozhi-server/core/api/ota_handler.py +++ b/main/xiaozhi-server/core/api/ota_handler.py @@ -9,10 +9,8 @@ import glob from typing import Dict, List, Tuple from aiohttp import web -from urllib.parse import urlparse - from core.auth import AuthManager -from core.utils.util import get_local_ip +from core.utils.util import get_local_ip, get_vision_url from core.api.base_handler import BaseHandler TAG = __name__ @@ -59,12 +57,18 @@ class OTAHandler(BaseHandler): # firmware storage self.bin_dir = os.path.join(os.getcwd(), "data", "bin") # cache structure: { 'updated_at': timestamp, 'ttl': seconds, 'files_by_model': { model: [(version, filename), ...] } } - self._bin_cache: Dict = {"updated_at": 0, "ttl": config.get("firmware_cache_ttl", 30), "files_by_model": {}} + self._bin_cache: Dict = { + "updated_at": 0, + "ttl": config.get("firmware_cache_ttl", 30), + "files_by_model": {}, + } def _refresh_bin_cache_if_needed(self): now = int(time.time()) ttl = int(self._bin_cache.get("ttl", 30)) - if now - int(self._bin_cache.get("updated_at", 0)) < ttl and self._bin_cache.get("files_by_model"): + if now - int( + self._bin_cache.get("updated_at", 0) + ) < ttl and self._bin_cache.get("files_by_model"): return files_by_model: Dict[str, List[Tuple[str, str]]] = {} @@ -91,7 +95,9 @@ class OTAHandler(BaseHandler): self._bin_cache["files_by_model"] = files_by_model self._bin_cache["updated_at"] = now - self.logger.bind(tag=TAG).info(f"Firmware cache refreshed: {len(files_by_model)} models") + self.logger.bind(tag=TAG).info( + f"Firmware cache refreshed: {len(files_by_model)} models" + ) except Exception as e: self.logger.bind(tag=TAG).error(f"刷新固件缓存失败: {e}") # keep previous cache if any @@ -175,18 +181,6 @@ class OTAHandler(BaseHandler): http_port = int(server_config.get("http_port", 8003)) local_ip = get_local_ip() - ota_addr = "" - websocket_addr = self._get_websocket_url(local_ip, websocket_port) - parsedurl = urlparse(websocket_addr) - netloc = parsedurl.netloc - if netloc: - host_part = netloc.split(":")[0] - ota_addr = host_part if host_part else "" - if ota_addr == "": - ota_addr = server_config.get("ota_addr", "") - if ota_addr == "": - ota_addr = local_ip - # Determine device model (prefer headers) device_model = "" # header candidates @@ -208,7 +202,13 @@ class OTAHandler(BaseHandler): # Determine device current version (prefer headers) device_version = "" - for h in ("device-version", "device_version", "firmware-version", "app-version", "application-version"): + for h in ( + "device-version", + "device_version", + "firmware-version", + "app-version", + "application-version", + ): if h in request.headers: device_version = request.headers.get(h, "").strip() break @@ -303,7 +303,9 @@ class OTAHandler(BaseHandler): files_by_model = self._bin_cache.get("files_by_model", {}) candidates = files_by_model.get(device_model, []) - self.logger.bind(tag=TAG).info(f"查找型号 {device_model} 的固件,找到 {len(candidates)} 个候选") + self.logger.bind(tag=TAG).info( + f"查找型号 {device_model} 的固件,找到 {len(candidates)} 个候选" + ) chosen_url = "" chosen_version = device_version @@ -313,16 +315,24 @@ class OTAHandler(BaseHandler): if _is_higher_version(ver, device_version): # build download url (only allow download via our download endpoint) chosen_version = ver - # use local_ip and http_port to construct url - chosen_url = f"http://{ota_addr}:{http_port}/xiaozhi/ota/download/{fname}" + # Use get_vision_url to get the base URL and replace the path + vision_url = get_vision_url(self.config) + # Replace the path from "/mcp/vision/explain" to "/xiaozhi/ota/download/{fname}" + chosen_url = vision_url.replace( + "/mcp/vision/explain", f"/xiaozhi/ota/download/{fname}" + ) break if chosen_url: return_json["firmware"]["version"] = chosen_version return_json["firmware"]["url"] = chosen_url - self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发固件 {chosen_version} -> {chosen_url}") + self.logger.bind(tag=TAG).info( + f"为设备 {device_id} 下发固件 {chosen_version} [如果地址前缀有误,请检查配置文件中的server.vision_explain]-> {chosen_url} " + ) else: - self.logger.bind(tag=TAG).info(f"设备 {device_id} 固件已是最新: {device_version}") + self.logger.bind(tag=TAG).info( + f"设备 {device_id} 固件已是最新: {device_version}" + ) except Exception as e: self.logger.bind(tag=TAG).error(f"检查固件版本时出错: {e}") @@ -381,7 +391,10 @@ class OTAHandler(BaseHandler): # ensure realpath is under bin_dir file_real = os.path.realpath(file_path) bin_dir_real = os.path.realpath(self.bin_dir) - if not file_real.startswith(bin_dir_real + os.sep) and file_real != bin_dir_real: + if ( + not file_real.startswith(bin_dir_real + os.sep) + and file_real != bin_dir_real + ): raise web.HTTPForbidden(text="forbidden") if not os.path.isfile(file_real): diff --git a/main/xiaozhi-server/core/api/vision_handler.py b/main/xiaozhi-server/core/api/vision_handler.py index a4817a84..28f48753 100644 --- a/main/xiaozhi-server/core/api/vision_handler.py +++ b/main/xiaozhi-server/core/api/vision_handler.py @@ -2,6 +2,7 @@ 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 @@ -16,10 +17,9 @@ TAG = __name__ MAX_FILE_SIZE = 5 * 1024 * 1024 -class VisionHandler: +class VisionHandler(BaseHandler): def __init__(self, config: dict): - self.config = config - self.logger = setup_logging() + super().__init__(config) # 初始化认证工具 self.auth = AuthToken(config["server"]["auth_key"]) @@ -172,11 +172,3 @@ class VisionHandler: finally: self._add_cors_headers(response) return response - - def _add_cors_headers(self, response): - """添加CORS头信息""" - response.headers["Access-Control-Allow-Headers"] = ( - "client-id, content-type, device-id" - ) - response.headers["Access-Control-Allow-Credentials"] = "true" - response.headers["Access-Control-Allow-Origin"] = "*" diff --git a/main/xiaozhi-server/core/http_server.py b/main/xiaozhi-server/core/http_server.py index ecc80efb..feb96f3b 100644 --- a/main/xiaozhi-server/core/http_server.py +++ b/main/xiaozhi-server/core/http_server.py @@ -33,41 +33,60 @@ class SimpleHttpServer: return f"ws://{local_ip}:{port}/xiaozhi/v1/" async def start(self): - server_config = self.config["server"] - read_config_from_api = self.config.get("read_config_from_api", False) - host = server_config.get("ip", "0.0.0.0") - port = int(server_config.get("http_port", 8003)) + try: + server_config = self.config["server"] + read_config_from_api = self.config.get("read_config_from_api", False) + host = server_config.get("ip", "0.0.0.0") + port = int(server_config.get("http_port", 8003)) - if port: - app = web.Application() + if port: + app = web.Application() - if not read_config_from_api: - # 如果没有开启智控台,只是单模块运行,就需要再添加简单OTA接口,用于下发websocket接口 + if not read_config_from_api: + # 如果没有开启智控台,只是单模块运行,就需要再添加简单OTA接口,用于下发websocket接口 + app.add_routes( + [ + web.get("/xiaozhi/ota/", self.ota_handler.handle_get), + web.post("/xiaozhi/ota/", self.ota_handler.handle_post), + web.options( + "/xiaozhi/ota/", self.ota_handler.handle_options + ), + # 下载接口,仅提供 data/bin/*.bin 下载 + web.get( + "/xiaozhi/ota/download/{filename}", + self.ota_handler.handle_download, + ), + web.options( + "/xiaozhi/ota/download/{filename}", + self.ota_handler.handle_options, + ), + ] + ) + # 添加路由 app.add_routes( [ - web.get("/xiaozhi/ota/", self.ota_handler.handle_get), - web.post("/xiaozhi/ota/", self.ota_handler.handle_post), - web.options("/xiaozhi/ota/", self.ota_handler.handle_post), - # 下载接口,仅提供 data/bin/*.bin 下载 - web.get("/xiaozhi/ota/download/{filename}", self.ota_handler.handle_download), - web.options("/xiaozhi/ota/download/{filename}", self.ota_handler.handle_download), + web.get("/mcp/vision/explain", self.vision_handler.handle_get), + web.post( + "/mcp/vision/explain", self.vision_handler.handle_post + ), + web.options( + "/mcp/vision/explain", self.vision_handler.handle_options + ), ] ) - # 添加路由 - app.add_routes( - [ - web.get("/mcp/vision/explain", self.vision_handler.handle_get), - web.post("/mcp/vision/explain", self.vision_handler.handle_post), - web.options("/mcp/vision/explain", self.vision_handler.handle_post), - ] - ) - # 运行服务 - runner = web.AppRunner(app) - await runner.setup() - site = web.TCPSite(runner, host, port) - await site.start() + # 运行服务 + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, host, port) + await site.start() - # 保持服务运行 - while True: - await asyncio.sleep(3600) # 每隔 1 小时检查一次 \ No newline at end of file + # 保持服务运行 + while True: + await asyncio.sleep(3600) # 每隔 1 小时检查一次 + except Exception as e: + self.logger.bind(tag=TAG).error(f"HTTP服务器启动失败: {e}") + import traceback + + self.logger.bind(tag=TAG).error(f"错误堆栈: {traceback.format_exc()}") + raise