update:从vision_url配置里读取域名和端口号

This commit is contained in:
hrz
2025-12-18 17:37:20 +08:00
parent f5565f6700
commit 6e7c86e159
4 changed files with 99 additions and 67 deletions
+9 -1
View File
@@ -10,7 +10,15 @@ class BaseHandler:
def _add_cors_headers(self, response): def _add_cors_headers(self, response):
"""添加CORS头信息""" """添加CORS头信息"""
response.headers["Access-Control-Allow-Headers"] = ( 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-Credentials"] = "true"
response.headers["Access-Control-Allow-Origin"] = "*" 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
+38 -25
View File
@@ -9,10 +9,8 @@ import glob
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
from aiohttp import web from aiohttp import web
from urllib.parse import urlparse
from core.auth import AuthManager 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 from core.api.base_handler import BaseHandler
TAG = __name__ TAG = __name__
@@ -59,12 +57,18 @@ class OTAHandler(BaseHandler):
# firmware storage # firmware storage
self.bin_dir = os.path.join(os.getcwd(), "data", "bin") self.bin_dir = os.path.join(os.getcwd(), "data", "bin")
# cache structure: { 'updated_at': timestamp, 'ttl': seconds, 'files_by_model': { model: [(version, filename), ...] } } # 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): def _refresh_bin_cache_if_needed(self):
now = int(time.time()) now = int(time.time())
ttl = int(self._bin_cache.get("ttl", 30)) 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 return
files_by_model: Dict[str, List[Tuple[str, str]]] = {} 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["files_by_model"] = files_by_model
self._bin_cache["updated_at"] = now 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: except Exception as e:
self.logger.bind(tag=TAG).error(f"刷新固件缓存失败: {e}") self.logger.bind(tag=TAG).error(f"刷新固件缓存失败: {e}")
# keep previous cache if any # keep previous cache if any
@@ -175,18 +181,6 @@ class OTAHandler(BaseHandler):
http_port = int(server_config.get("http_port", 8003)) http_port = int(server_config.get("http_port", 8003))
local_ip = get_local_ip() 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) # Determine device model (prefer headers)
device_model = "" device_model = ""
# header candidates # header candidates
@@ -208,7 +202,13 @@ class OTAHandler(BaseHandler):
# Determine device current version (prefer headers) # Determine device current version (prefer headers)
device_version = "" 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: if h in request.headers:
device_version = request.headers.get(h, "").strip() device_version = request.headers.get(h, "").strip()
break break
@@ -303,7 +303,9 @@ class OTAHandler(BaseHandler):
files_by_model = self._bin_cache.get("files_by_model", {}) files_by_model = self._bin_cache.get("files_by_model", {})
candidates = files_by_model.get(device_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_url = ""
chosen_version = device_version chosen_version = device_version
@@ -313,16 +315,24 @@ class OTAHandler(BaseHandler):
if _is_higher_version(ver, device_version): if _is_higher_version(ver, device_version):
# build download url (only allow download via our download endpoint) # build download url (only allow download via our download endpoint)
chosen_version = ver chosen_version = ver
# use local_ip and http_port to construct url # Use get_vision_url to get the base URL and replace the path
chosen_url = f"http://{ota_addr}:{http_port}/xiaozhi/ota/download/{fname}" 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 break
if chosen_url: if chosen_url:
return_json["firmware"]["version"] = chosen_version return_json["firmware"]["version"] = chosen_version
return_json["firmware"]["url"] = chosen_url 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: 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: except Exception as e:
self.logger.bind(tag=TAG).error(f"检查固件版本时出错: {e}") self.logger.bind(tag=TAG).error(f"检查固件版本时出错: {e}")
@@ -381,7 +391,10 @@ class OTAHandler(BaseHandler):
# ensure realpath is under bin_dir # ensure realpath is under bin_dir
file_real = os.path.realpath(file_path) file_real = os.path.realpath(file_path)
bin_dir_real = os.path.realpath(self.bin_dir) 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") raise web.HTTPForbidden(text="forbidden")
if not os.path.isfile(file_real): if not os.path.isfile(file_real):
+3 -11
View File
@@ -2,6 +2,7 @@ import json
import copy import copy
from aiohttp import web from aiohttp import web
from config.logger import setup_logging 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.util import get_vision_url, is_valid_image_file
from core.utils.vllm import create_instance from core.utils.vllm import create_instance
from config.config_loader import get_private_config_from_api from config.config_loader import get_private_config_from_api
@@ -16,10 +17,9 @@ TAG = __name__
MAX_FILE_SIZE = 5 * 1024 * 1024 MAX_FILE_SIZE = 5 * 1024 * 1024
class VisionHandler: class VisionHandler(BaseHandler):
def __init__(self, config: dict): def __init__(self, config: dict):
self.config = config super().__init__(config)
self.logger = setup_logging()
# 初始化认证工具 # 初始化认证工具
self.auth = AuthToken(config["server"]["auth_key"]) self.auth = AuthToken(config["server"]["auth_key"])
@@ -172,11 +172,3 @@ class VisionHandler:
finally: finally:
self._add_cors_headers(response) self._add_cors_headers(response)
return 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"] = "*"
+49 -30
View File
@@ -33,41 +33,60 @@ class SimpleHttpServer:
return f"ws://{local_ip}:{port}/xiaozhi/v1/" return f"ws://{local_ip}:{port}/xiaozhi/v1/"
async def start(self): async def start(self):
server_config = self.config["server"] try:
read_config_from_api = self.config.get("read_config_from_api", False) server_config = self.config["server"]
host = server_config.get("ip", "0.0.0.0") read_config_from_api = self.config.get("read_config_from_api", False)
port = int(server_config.get("http_port", 8003)) host = server_config.get("ip", "0.0.0.0")
port = int(server_config.get("http_port", 8003))
if port: if port:
app = web.Application() app = web.Application()
if not read_config_from_api: if not read_config_from_api:
# 如果没有开启智控台,只是单模块运行,就需要再添加简单OTA接口,用于下发websocket接口 # 如果没有开启智控台,只是单模块运行,就需要再添加简单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( app.add_routes(
[ [
web.get("/xiaozhi/ota/", self.ota_handler.handle_get), web.get("/mcp/vision/explain", self.vision_handler.handle_get),
web.post("/xiaozhi/ota/", self.ota_handler.handle_post), web.post(
web.options("/xiaozhi/ota/", self.ota_handler.handle_post), "/mcp/vision/explain", self.vision_handler.handle_post
# 下载接口,仅提供 data/bin/*.bin 下载 ),
web.get("/xiaozhi/ota/download/{filename}", self.ota_handler.handle_download), web.options(
web.options("/xiaozhi/ota/download/{filename}", self.ota_handler.handle_download), "/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) runner = web.AppRunner(app)
await runner.setup() await runner.setup()
site = web.TCPSite(runner, host, port) site = web.TCPSite(runner, host, port)
await site.start() await site.start()
# 保持服务运行 # 保持服务运行
while True: while True:
await asyncio.sleep(3600) # 每隔 1 小时检查一次 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