mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 17:43:55 +08:00
update:从vision_url配置里读取域名和端口号
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"] = "*"
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user