From 33a385cfa8b4f4bae0a7155326d1a2bee8b0a506 Mon Sep 17 00:00:00 2001 From: rui chen Date: Wed, 17 Dec 2025 16:26:35 +0800 Subject: [PATCH 1/7] add basic OTA support for single server deployment Committer: rxchen --- main/xiaozhi-server/core/api/ota_handler.py | 219 ++++++++++++++++++-- main/xiaozhi-server/core/http_server.py | 5 +- 2 files changed, 209 insertions(+), 15 deletions(-) diff --git a/main/xiaozhi-server/core/api/ota_handler.py b/main/xiaozhi-server/core/api/ota_handler.py index b6c88dff..a521332e 100644 --- a/main/xiaozhi-server/core/api/ota_handler.py +++ b/main/xiaozhi-server/core/api/ota_handler.py @@ -3,6 +3,10 @@ import time import base64 import hashlib import hmac +import os +import re +import glob +from typing import Dict, List, Tuple from aiohttp import web from core.auth import AuthManager @@ -12,6 +16,33 @@ from core.api.base_handler import BaseHandler TAG = __name__ +def _safe_basename(filename: str) -> str: + # Prevent directory traversal + return os.path.basename(filename) + + +def _parse_version(ver: str) -> Tuple[int, ...]: + # conservative parser: split by non-digit, keep numeric parts + parts = re.findall(r"\d+", ver) + return tuple(int(p) for p in parts) if parts else (0,) + + +def _is_higher_version(a: str, b: str) -> bool: + """Return True if version string a > b (semver-like numeric compare).""" + ta = _parse_version(a) + tb = _parse_version(b) + # compare tuple lexicographically, but allow different lengths + maxlen = max(len(ta), len(tb)) + for i in range(maxlen): + ai = ta[i] if i < len(ta) else 0 + bi = tb[i] if i < len(tb) else 0 + if ai > bi: + return True + if ai < bi: + return False + return False + + class OTAHandler(BaseHandler): def __init__(self, config: dict): super().__init__(config) @@ -23,6 +54,46 @@ class OTAHandler(BaseHandler): expire_seconds = auth_config.get("expire_seconds") self.auth = AuthManager(secret_key=secret_key, expire_seconds=expire_seconds) + # 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": {}} + + 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"): + return + + files_by_model: Dict[str, List[Tuple[str, str]]] = {} + try: + if not os.path.isdir(self.bin_dir): + os.makedirs(self.bin_dir, exist_ok=True) + + # match files like model_1.2.3.bin (allow dots, dashes, underscores in model and version) + pattern = os.path.join(self.bin_dir, "*.bin") + for path in glob.glob(pattern): + fname = os.path.basename(path) + # filename format: {model}_{version}.bin + m = re.match(r"^(.+?)_([0-9][A-Za-z0-9\.\-_]*)\.bin$", fname) + if not m: + # skip files not conforming to naming rule + continue + model = m.group(1) + version = m.group(2) + files_by_model.setdefault(model, []).append((version, fname)) + + # sort versions for each model descending + for model, items in files_by_model.items(): + items.sort(key=lambda it: _parse_version(it[0]), reverse=True) + + 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") + except Exception as e: + self.logger.bind(tag=TAG).error(f"刷新固件缓存失败: {e}") + # keep previous cache if any + def generate_password_signature(self, content: str, secret_key: str) -> str: """生成MQTT密码签名 @@ -62,7 +133,14 @@ class OTAHandler(BaseHandler): return f"ws://{local_ip}:{port}/xiaozhi/v1/" async def handle_post(self, request): - """处理 OTA POST 请求""" + """处理 OTA POST 请求 + + This handler will: + - read device id/client id (as before) + - attempt to determine device model and current firmware version (prefer headers, fallback to body) + - check data/bin for newer firmware for that model + - if found a newer firmware, set firmware.url to the download endpoint + """ try: data = await request.text() self.logger.bind(tag=TAG).debug(f"OTA请求方法: {request.method}") @@ -81,11 +159,54 @@ class OTAHandler(BaseHandler): else: raise Exception("OTA请求ClientID为空") - data_json = json.loads(data) + data_json = {} + try: + data_json = json.loads(data) if data else {} + self.logger.bind(tag=TAG).info(f"data json:{data_json}") + except Exception: + data_json = {} server_config = self.config["server"] - port = int(server_config.get("port", 8000)) + # Distinguish ports: + # - websocket_port is used to construct websocket URL (server["port"]) + # - http_port is used to construct OTA download URLs (server["http_port"]) + websocket_port = int(server_config.get("port", 8000)) + http_port = int(server_config.get("http_port", 8003)) local_ip = get_local_ip() + ota_addr = server_config.get("ota_addr", "") + + # Determine device model (prefer headers) + device_model = "" + # header candidates + for h in ("device-model", "device_model", "model"): + if h in request.headers: + device_model = request.headers.get(h, "").strip() + break + # body fallback + if not device_model: + try: + if "board" in data_json and isinstance(data_json["board"], dict): + device_model = data_json["board"].get("type", "") + elif "model" in data_json: + device_model = data_json.get("model", "") + except Exception: + device_model = "" + if not device_model: + device_model = "default" + + # Determine device current version (prefer headers) + device_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 + if not device_version: + try: + device_version = data_json.get("application", {}).get("version", "") + except Exception: + device_version = "" + if not device_version: + device_version = "0.0.0" return_json = { "server_time": { @@ -93,21 +214,17 @@ class OTAHandler(BaseHandler): "timezone_offset": server_config.get("timezone_offset", 8) * 60, }, "firmware": { - "version": data_json["application"].get("version", "1.0.0"), + "version": device_version, "url": "", }, } + # existing mqtt/websocket logic (unchanged) mqtt_gateway_endpoint = server_config.get("mqtt_gateway") if mqtt_gateway_endpoint: # 如果配置了非空字符串 - # 尝试从请求数据中获取设备型号 - device_model = "default" + # 尝试从请求数据中获取设备型号(已解析 above) try: - if "device" in data_json and isinstance(data_json["device"], dict): - device_model = data_json["device"].get("model", "default") - elif "model" in data_json: - device_model = data_json["model"] group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_") except Exception as e: self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}") @@ -159,20 +276,51 @@ class OTAHandler(BaseHandler): token = self.auth.generate_token(client_id, device_id) else: token = self.auth.generate_token(client_id, device_id) + # NOTE: use websocket_port here return_json["websocket"] = { - "url": self._get_websocket_url(local_ip, port), + "url": self._get_websocket_url(local_ip, websocket_port), "token": token, } self.logger.bind(tag=TAG).info( f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置" ) - self.logger.bind(tag=TAG).info(f"{return_json}") + + # Now check firmware files for updates + try: + self._refresh_bin_cache_if_needed() + 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)} 个候选") + + chosen_url = "" + chosen_version = device_version + + # candidates are sorted descending by version + for ver, fname in candidates: + 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}" + 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}") + else: + self.logger.bind(tag=TAG).info(f"设备 {device_id} 固件已是最新: {device_version}") + + except Exception as e: + self.logger.bind(tag=TAG).error(f"检查固件版本时出错: {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"OTA POST处理异常: {e}") return_json = {"success": False, "message": "request error."} response = web.Response( text=json.dumps(return_json, separators=(",", ":")), @@ -187,8 +335,9 @@ class OTAHandler(BaseHandler): try: server_config = self.config["server"] local_ip = get_local_ip() - port = int(server_config.get("port", 8000)) - websocket_url = self._get_websocket_url(local_ip, port) + # use websocket port for websocket URL + websocket_port = int(server_config.get("port", 8000)) + websocket_url = self._get_websocket_url(local_ip, websocket_port) message = f"OTA接口运行正常,向设备发送的websocket地址是:{websocket_url}" response = web.Response(text=message, content_type="text/plain") except Exception as e: @@ -197,3 +346,45 @@ class OTAHandler(BaseHandler): finally: self._add_cors_headers(response) return response + + async def handle_download(self, request): + """ + 下载固件接口 + URL: /xiaozhi/ota/download/{filename} + - 只允许下载 data/bin 目录下的 .bin 文件 + - filename 必须是 basename 且匹配安全的模式 + """ + try: + fname = request.match_info.get("filename", "") + if not fname: + raise web.HTTPBadRequest(text="filename required") + + # sanitize + fname = _safe_basename(fname) + # pattern: allow letters, numbers, dot, underscore, dash + if not re.match(r"^[A-Za-z0-9\.\-_]+\.bin$", fname): + raise web.HTTPBadRequest(text="invalid filename") + + file_path = os.path.join(self.bin_dir, fname) + # 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: + raise web.HTTPForbidden(text="forbidden") + + if not os.path.isfile(file_real): + raise web.HTTPNotFound(text="file not found") + + # use FileResponse to stream file + resp = web.FileResponse(path=file_real) + except web.HTTPError as e: + resp = e + except Exception as e: + self.logger.bind(tag=TAG).error(f"固件下载异常: {e}") + resp = web.Response(text="download error", status=500) + finally: + try: + self._add_cors_headers(resp) + except Exception: + pass + return resp diff --git a/main/xiaozhi-server/core/http_server.py b/main/xiaozhi-server/core/http_server.py index edbdf1fe..ecc80efb 100644 --- a/main/xiaozhi-server/core/http_server.py +++ b/main/xiaozhi-server/core/http_server.py @@ -48,6 +48,9 @@ class SimpleHttpServer: 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), ] ) # 添加路由 @@ -67,4 +70,4 @@ class SimpleHttpServer: # 保持服务运行 while True: - await asyncio.sleep(3600) # 每隔 1 小时检查一次 + await asyncio.sleep(3600) # 每隔 1 小时检查一次 \ No newline at end of file From d5f804bbb36c736edd5b8e1e7c5d6b13793acb9f Mon Sep 17 00:00:00 2001 From: rui chen Date: Wed, 17 Dec 2025 16:44:54 +0800 Subject: [PATCH 2/7] add basic OTA support for single server deployment, remove debug --- main/xiaozhi-server/core/api/ota_handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/main/xiaozhi-server/core/api/ota_handler.py b/main/xiaozhi-server/core/api/ota_handler.py index a521332e..b20ba60b 100644 --- a/main/xiaozhi-server/core/api/ota_handler.py +++ b/main/xiaozhi-server/core/api/ota_handler.py @@ -162,7 +162,6 @@ class OTAHandler(BaseHandler): data_json = {} try: data_json = json.loads(data) if data else {} - self.logger.bind(tag=TAG).info(f"data json:{data_json}") except Exception: data_json = {} From 33d70ccc96c54ac2a6ef044bfc7d3b43be77a427 Mon Sep 17 00:00:00 2001 From: rui chen Date: Thu, 18 Dec 2025 15:48:25 +0800 Subject: [PATCH 3/7] get OTA address from websocket address config, if failed find ota_addr, if failed again, use local address. --- main/xiaozhi-server/core/api/ota_handler.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/main/xiaozhi-server/core/api/ota_handler.py b/main/xiaozhi-server/core/api/ota_handler.py index b20ba60b..1fe339c2 100644 --- a/main/xiaozhi-server/core/api/ota_handler.py +++ b/main/xiaozhi-server/core/api/ota_handler.py @@ -9,6 +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.api.base_handler import BaseHandler @@ -172,7 +174,18 @@ class OTAHandler(BaseHandler): websocket_port = int(server_config.get("port", 8000)) http_port = int(server_config.get("http_port", 8003)) local_ip = get_local_ip() - ota_addr = server_config.get("ota_addr", "") + + 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 = "" 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 4/7] =?UTF-8?q?update:=E4=BB=8Evision=5Furl=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=87=8C=E8=AF=BB=E5=8F=96=E5=9F=9F=E5=90=8D=E5=92=8C?= =?UTF-8?q?=E7=AB=AF=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 From f3f0d62f1269102c4af5a7bfcf4ffa85355ad0ee Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 18 Dec 2025 17:38:58 +0800 Subject: [PATCH 5/7] =?UTF-8?q?add:=E6=B7=BB=E5=8A=A0=E5=8D=95=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E9=83=A8=E7=BD=B2=E6=97=B6=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?ota=E6=8E=A5=E5=8F=A3=E8=87=AA=E5=8A=A8=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E5=9B=BA=E4=BB=B6=E7=9A=84=E6=95=99=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FAQ.md | 1 + docs/ota-upgrade-guide.md | 264 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 docs/ota-upgrade-guide.md diff --git a/docs/FAQ.md b/docs/FAQ.md index f1e12d84..69494c3d 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -69,6 +69,7 @@ VAD: ### 9、编译固件相关教程 1、[如何自己编译小智固件](./firmware-build.md)
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)
+3、[单模块部署如何配置固件OTA自动升级](./ota-upgrade-guide.md)
### 10、拓展相关教程 1、[如何开启手机号码注册智控台](./ali-sms-integration.md)
diff --git a/docs/ota-upgrade-guide.md b/docs/ota-upgrade-guide.md new file mode 100644 index 00000000..157a80ba --- /dev/null +++ b/docs/ota-upgrade-guide.md @@ -0,0 +1,264 @@ +# 单模块部署固件OTA自动升级配置指南 + +本教程将指导你如何在**单模块部署**场景下配置固件OTA自动升级功能,实现设备固件的自动更新。 + +## 功能介绍 + +在单模块部署中,xiaozhi-server内置了OTA固件管理功能,可以自动检测设备版本并下发升级固件。系统会根据设备型号和当前版本,自动匹配并推送最新的固件版本。 + +## 前提条件 + +- 你已经成功进行**单模块部署**并运行xiaozhi-server +- 设备能够正常连接到服务器 + +## 第一步 准备固件文件 + +### 1. 创建固件存放目录 + +固件文件需要放在`data/bin/`目录下。如果该目录不存在,请手动创建: + +```bash +mkdir -p data/bin +``` + +### 2. 固件文件命名规则 + +固件文件必须遵循以下命名格式: + +``` +{设备型号}_{版本号}.bin +``` + +**命名规则说明:** +- `设备型号`:设备的型号名称,例如 `lichuang-dev`、`bread-compact-wifi` 等 +- `版本号`:固件版本号,必须以数字开头,支持数字、字母、点号、下划线和短横线,例如 `1.6.6`、`2.0.0` 等 +- 文件扩展名必须是 `.bin` + +**命名示例:** +``` +bread-compact-wifi_1.6.6.bin +lichuang-dev_2.0.0.bin +``` + +### 3. 放置固件文件 + +将准备好的固件文件(.bin文件)复制到`data/bin/`目录下: + +```bash +cp your_firmware.bin data/bin/设备型号_版本号.bin +``` + +例如: +```bash +cp xiaozhi_firmware.bin data/bin/esp32s3_1.6.6.bin +``` + +## 第二步 配置公网访问地址(仅公网部署需要) + +**注意:此步骤仅适用于单模块公网部署的场景。** + +如果你的xiaozhi-server是公网部署(使用公网IP或域名),**必须**配置`server.vision_explain`参数,因为OTA固件下载地址会使用该配置的域名和端口。 + +如果你是局域网部署,可以跳过此步骤。 + +### 为什么要配置这个参数? + +在单模块部署中,系统生成固件下载地址时,会使用`vision_explain`配置的域名和端口作为基础地址。如果不配置或配置错误,设备将无法访问固件下载地址。 + +### 配置方法 + +打开`data/.config.yaml`文件,找到`server`配置段,设置`vision_explain`参数: + +```yaml +server: + vision_explain: http://你的域名或IP:端口号/mcp/vision/explain +``` + +**配置示例:** + +局域网部署(默认): +```yaml +server: + vision_explain: http://192.168.1.100:8003/mcp/vision/explain +``` + +公网域名部署: +```yaml +server: + vision_explain: http://yourdomain.com:8003/mcp/vision/explain +``` + +公网IP部署: +```yaml +server: + vision_explain: http://111.111.111.111:8003/mcp/vision/explain +``` + +使用HTTPS(推荐公网部署使用): +```yaml +server: + vision_explain: https://yourdomain.com:8003/mcp/vision/explain +``` + +### 注意事项 + +- 域名或IP必须是设备能够访问的地址 +- 如果使用Docker部署,不能使用Docker内部地址(如127.0.0.1或localhost) +- 端口号默认是8003,如果你修改了`server.http_port`配置,需要同步修改这里的端口号 + +## 第三步 启动或重启服务 + +### 源码运行 + +```bash +python app.py +``` + +### Docker运行 + +```bash +docker restart xiaozhi-esp32-server +``` + +### 验证服务启动 + +启动后,查看日志输出,应该能看到类似以下内容: + +``` +2025-12-18 **** - OTA接口是 http://192.168.1.100:8003/xiaozhi/ota/ +2025-12-18 **** - 视觉分析接口是 http://192.168.1.100:8003/mcp/vision/explain +``` + +使用浏览器访问OTA接口地址,如果显示以下内容说明服务正常: + +``` +OTA接口运行正常,向设备发送的websocket地址是:ws://xxx.xxx.xxx.xxx:8000/xiaozhi/v1/ +``` + +## 第四步 设备自动检测升级 + +### 升级原理 + +当设备连接到服务器时(每次开机或定时检查),会自动发送OTA请求。服务器会: + +1. 读取设备的型号和当前固件版本 +2. 扫描`data/bin/`目录,查找匹配该型号的所有固件文件 +3. 比较版本号,如果有更高版本,则返回固件下载地址 +4. 设备收到下载地址后,会自动下载并安装新固件 + +### 版本比较规则 + +系统使用语义化版本比较方式,按数字段从左到右依次比较: + +- `1.6.6` < `1.6.7` +- `1.6.9` < `1.7.0` +- `2.0.0` > `1.9.9` + +### 查看升级日志 + +在xiaozhi-server的日志中,你可以看到OTA相关的日志输出: + +``` +[ota_handler] - OTA请求设备ID: AA:BB:CC:DD:EE:FF +[ota_handler] - 查找型号 esp32s3 的固件,找到 3 个候选 +[ota_handler] - 为设备 AA:BB:CC:DD:EE:FF 下发固件 1.6.6 [如果地址前缀有误,请检查配置文件中的server.vision_explain]-> http://yourdomain.com:8003/xiaozhi/ota/download/esp32s3_1.6.6.bin +``` + +或者如果设备已是最新版本: + +``` +[ota_handler] - 设备 AA:BB:CC:DD:EE:FF 固件已是最新: 1.6.6 +``` + +## 高级配置 + +### 固件缓存时间(可选) + +系统会缓存`data/bin/`目录的扫描结果以提高性能。默认缓存时间为30秒。你可以在配置文件中调整: + +```yaml +firmware_cache_ttl: 60 # 单位:秒,设置为60秒缓存时间 +``` + +### 多版本固件管理 + +你可以同时放置多个版本的固件,系统会自动选择最新版本: + +``` +data/bin/ + ├── esp32s3_1.6.5.bin + ├── esp32s3_1.6.6.bin + ├── esp32s3_1.7.0-beta.bin + └── xiaozhi-v2_2.0.0.bin +``` + +系统会为`esp32s3`型号的设备推送`1.7.0-beta`版本(最高版本)。 + +### 多型号固件管理 + +不同型号的设备会自动匹配对应型号的固件: + +``` +data/bin/ + ├── esp32s3_1.6.6.bin # 仅供 esp32s3 型号设备使用 + ├── xiaozhi-v2_2.0.0.bin # 仅供 xiaozhi-v2 型号设备使用 + └── default_1.0.0.bin # 供未识别型号的设备使用 +``` + +## 常见问题 + +### 1. 设备收不到固件更新 + +**可能原因和解决方法:** + +- 检查固件文件命名是否符合规则:`{型号}_{版本号}.bin` +- 检查固件文件是否正确放置在`data/bin/`目录 +- 检查设备型号是否与固件文件名中的型号匹配 +- 检查固件版本号是否高于设备当前版本 +- 查看服务器日志,确认OTA请求是否正常处理 + +### 2. 设备报告下载地址无法访问 + +**可能原因和解决方法:** + +- 检查`server.vision_explain`配置的域名或IP是否正确 +- 确认端口号配置正确(默认8003) +- 如果是公网部署,确保设备能够访问该公网地址 +- 如果是Docker部署,确保不是使用了内部地址(127.0.0.1) +- 检查防火墙是否开放了对应端口 + +### 3. 如何确认设备当前版本 + +查看OTA请求日志,日志中会显示设备上报的版本号: + +``` +[ota_handler] - 设备 AA:BB:CC:DD:EE:FF 固件已是最新: 1.6.6 +``` + +### 4. 固件文件放置后没有生效 + +系统有30秒的缓存时间(默认),可以: +- 等待30秒后再让设备发起OTA请求 +- 重启xiaozhi-server服务 +- 调整`firmware_cache_ttl`配置为更短的时间 + +### 5. 如何回滚到旧版本 + +系统只会推送更高版本的固件。如果需要回滚: +1. 删除或重命名`data/bin/`目录中高于目标版本的固件文件 +2. 等待缓存过期或重启服务 +3. 设备下次检查时会收到目标版本 + +## 安全说明 + +- 系统会验证固件文件路径,防止目录穿越攻击 +- 固件下载接口只允许访问`data/bin/`目录下的`.bin`文件 +- 建议在生产环境使用HTTPS协议传输固件 + +## 相关教程 + +如需了解更多,请参考以下教程: + +1. [如何自己编译小智固件](./firmware-build.md) +2. [如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md) +3. [如何进行全模块部署](./Deployment_all.md) From 7222f68d4d994c06acdb9443ba1fb7ee4c82f7b9 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 18 Dec 2025 17:57:05 +0800 Subject: [PATCH 6/7] =?UTF-8?q?update:=E6=B7=BB=E5=8A=A0=E9=87=8D=E8=A6=81?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ota-upgrade-guide.md | 146 ++++---------------------------------- 1 file changed, 12 insertions(+), 134 deletions(-) diff --git a/docs/ota-upgrade-guide.md b/docs/ota-upgrade-guide.md index 157a80ba..0df4a2ff 100644 --- a/docs/ota-upgrade-guide.md +++ b/docs/ota-upgrade-guide.md @@ -2,6 +2,8 @@ 本教程将指导你如何在**单模块部署**场景下配置固件OTA自动升级功能,实现设备固件的自动更新。 +如果你已经使用**全模块部署**,请忽略本教程。 + ## 功能介绍 在单模块部署中,xiaozhi-server内置了OTA固件管理功能,可以自动检测设备版本并下发升级固件。系统会根据设备型号和当前版本,自动匹配并推送最新的固件版本。 @@ -44,13 +46,19 @@ lichuang-dev_2.0.0.bin 将准备好的固件文件(.bin文件)复制到`data/bin/`目录下: +重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`! + +重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`! + +重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`! + ```bash -cp your_firmware.bin data/bin/设备型号_版本号.bin +cp xiaozhi.bin data/bin/设备型号_版本号.bin ``` 例如: ```bash -cp xiaozhi_firmware.bin data/bin/esp32s3_1.6.6.bin +cp xiaozhi.bin data/bin/bread-compact-wifi_1.6.6.bin ``` ## 第二步 配置公网访问地址(仅公网部署需要) @@ -88,122 +96,12 @@ server: vision_explain: http://yourdomain.com:8003/mcp/vision/explain ``` -公网IP部署: -```yaml -server: - vision_explain: http://111.111.111.111:8003/mcp/vision/explain -``` - -使用HTTPS(推荐公网部署使用): -```yaml -server: - vision_explain: https://yourdomain.com:8003/mcp/vision/explain -``` - ### 注意事项 - 域名或IP必须是设备能够访问的地址 - 如果使用Docker部署,不能使用Docker内部地址(如127.0.0.1或localhost) -- 端口号默认是8003,如果你修改了`server.http_port`配置,需要同步修改这里的端口号 +- 如果你使用了nginx反向代理,请填写对外的地址和端口号,不是本项目运行的端口号 -## 第三步 启动或重启服务 - -### 源码运行 - -```bash -python app.py -``` - -### Docker运行 - -```bash -docker restart xiaozhi-esp32-server -``` - -### 验证服务启动 - -启动后,查看日志输出,应该能看到类似以下内容: - -``` -2025-12-18 **** - OTA接口是 http://192.168.1.100:8003/xiaozhi/ota/ -2025-12-18 **** - 视觉分析接口是 http://192.168.1.100:8003/mcp/vision/explain -``` - -使用浏览器访问OTA接口地址,如果显示以下内容说明服务正常: - -``` -OTA接口运行正常,向设备发送的websocket地址是:ws://xxx.xxx.xxx.xxx:8000/xiaozhi/v1/ -``` - -## 第四步 设备自动检测升级 - -### 升级原理 - -当设备连接到服务器时(每次开机或定时检查),会自动发送OTA请求。服务器会: - -1. 读取设备的型号和当前固件版本 -2. 扫描`data/bin/`目录,查找匹配该型号的所有固件文件 -3. 比较版本号,如果有更高版本,则返回固件下载地址 -4. 设备收到下载地址后,会自动下载并安装新固件 - -### 版本比较规则 - -系统使用语义化版本比较方式,按数字段从左到右依次比较: - -- `1.6.6` < `1.6.7` -- `1.6.9` < `1.7.0` -- `2.0.0` > `1.9.9` - -### 查看升级日志 - -在xiaozhi-server的日志中,你可以看到OTA相关的日志输出: - -``` -[ota_handler] - OTA请求设备ID: AA:BB:CC:DD:EE:FF -[ota_handler] - 查找型号 esp32s3 的固件,找到 3 个候选 -[ota_handler] - 为设备 AA:BB:CC:DD:EE:FF 下发固件 1.6.6 [如果地址前缀有误,请检查配置文件中的server.vision_explain]-> http://yourdomain.com:8003/xiaozhi/ota/download/esp32s3_1.6.6.bin -``` - -或者如果设备已是最新版本: - -``` -[ota_handler] - 设备 AA:BB:CC:DD:EE:FF 固件已是最新: 1.6.6 -``` - -## 高级配置 - -### 固件缓存时间(可选) - -系统会缓存`data/bin/`目录的扫描结果以提高性能。默认缓存时间为30秒。你可以在配置文件中调整: - -```yaml -firmware_cache_ttl: 60 # 单位:秒,设置为60秒缓存时间 -``` - -### 多版本固件管理 - -你可以同时放置多个版本的固件,系统会自动选择最新版本: - -``` -data/bin/ - ├── esp32s3_1.6.5.bin - ├── esp32s3_1.6.6.bin - ├── esp32s3_1.7.0-beta.bin - └── xiaozhi-v2_2.0.0.bin -``` - -系统会为`esp32s3`型号的设备推送`1.7.0-beta`版本(最高版本)。 - -### 多型号固件管理 - -不同型号的设备会自动匹配对应型号的固件: - -``` -data/bin/ - ├── esp32s3_1.6.6.bin # 仅供 esp32s3 型号设备使用 - ├── xiaozhi-v2_2.0.0.bin # 仅供 xiaozhi-v2 型号设备使用 - └── default_1.0.0.bin # 供未识别型号的设备使用 -``` ## 常见问题 @@ -226,6 +124,7 @@ data/bin/ - 如果是公网部署,确保设备能够访问该公网地址 - 如果是Docker部署,确保不是使用了内部地址(127.0.0.1) - 检查防火墙是否开放了对应端口 +- 如果你使用了nginx反向代理,请填写对外的地址和端口号,不是本项目运行的端口号 ### 3. 如何确认设备当前版本 @@ -241,24 +140,3 @@ data/bin/ - 等待30秒后再让设备发起OTA请求 - 重启xiaozhi-server服务 - 调整`firmware_cache_ttl`配置为更短的时间 - -### 5. 如何回滚到旧版本 - -系统只会推送更高版本的固件。如果需要回滚: -1. 删除或重命名`data/bin/`目录中高于目标版本的固件文件 -2. 等待缓存过期或重启服务 -3. 设备下次检查时会收到目标版本 - -## 安全说明 - -- 系统会验证固件文件路径,防止目录穿越攻击 -- 固件下载接口只允许访问`data/bin/`目录下的`.bin`文件 -- 建议在生产环境使用HTTPS协议传输固件 - -## 相关教程 - -如需了解更多,请参考以下教程: - -1. [如何自己编译小智固件](./firmware-build.md) -2. [如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md) -3. [如何进行全模块部署](./Deployment_all.md) From ce49b409ac5520d38eb146cd5b25309536a76537 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 18 Dec 2025 18:11:53 +0800 Subject: [PATCH 7/7] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- README_de.md | 4 ++-- README_en.md | 4 ++-- README_vi.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7602db54..eb718c24 100644 --- a/README.md +++ b/README.md @@ -183,8 +183,8 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology #### 🚀 部署方式选择 | 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 | |---------|------|---------|---------|---------|---------| -| **最简化安装** | 智能对话、IOT、MCP、视觉感知 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - | -| **全模块安装** | 智能对话、IOT、MCP接入点、声纹识别、视觉感知、OTA、智控台 | 完整功能体验,数据存储在数据库 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) | +| **最简化安装** | 智能对话、单智能体管理 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - | +| **全模块安装** | 智能对话、多用户管理、多智能体管理、智控台界面操作 | 完整功能体验,数据存储在数据库 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) | 常见问题及相关教程,可参考[这个链接](./docs/FAQ.md) diff --git a/README_de.md b/README_de.md index 53609462..0e4c74d5 100644 --- a/README_de.md +++ b/README_de.md @@ -181,8 +181,8 @@ Dieses Projekt bietet zwei Bereitstellungsmethoden. Bitte wählen Sie basierend #### 🚀 Auswahl der Bereitstellungsmethode | Bereitstellungsmethode | Funktionen | Anwendungsszenarien | Deployment-Dokumente | Konfigurationsanforderungen | Video-Tutorials | |---------|------|---------|---------|---------|---------| -| **Vereinfachte Installation** | Intelligenter Dialog, IOT, MCP, visuelle Wahrnehmung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - | -| **Vollständige Modulinstallation** | Intelligenter Dialog, IOT, MCP-Endpunkte, Stimmabdruckerkennung, visuelle Wahrnehmung, OTA, intelligente Steuerkonsole | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①Docker-Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Quellcode-Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Quellcode-Deployment Auto-Update-Tutorial](./docs/dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) | +| **Vereinfachte Installation** | Intelligenter Dialog, Einzel-Agenten-Verwaltung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - | +| **Vollständige Modulinstallation** | Intelligenter Dialog, Mehrbenutzerverwaltung, Mehr-Agenten-Verwaltung, Intelligente Steuerkonsole-Bedienung | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①Docker-Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Quellcode-Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Quellcode-Deployment Auto-Update-Tutorial](./docs/dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) | Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](./docs/FAQ.md) diff --git a/README_en.md b/README_en.md index cc0f170c..196cf16f 100644 --- a/README_en.md +++ b/README_en.md @@ -181,8 +181,8 @@ This project provides two deployment methods. Please choose based on your specif #### 🚀 Deployment Method Selection | Deployment Method | Features | Applicable Scenarios | Deployment Docs | Configuration Requirements | Video Tutorials | |---------|------|---------|---------|---------|---------| -| **Simplified Installation** | Intelligent dialogue, IOT, MCP, visual perception | Low-configuration environments, data stored in config files, no database required | [①Docker Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Source Code Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - | -| **Full Module Installation** | Intelligent dialogue, IOT, MCP endpoints, voiceprint recognition, visual perception, OTA, intelligent control console | Complete functionality experience, data stored in database |[①Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) | +| **Simplified Installation** | Intelligent dialogue, single agent management | Low-configuration environments, data stored in config files, no database required | [①Docker Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Source Code Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - | +| **Full Module Installation** | Intelligent dialogue, multi-user management, multi-agent management, intelligent console interface operation | Complete functionality experience, data stored in database |[①Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) | > 💡 Note: Below is a test platform deployed with the latest code. You can burn and test if needed. Concurrent users: 6, data will be cleared daily. diff --git a/README_vi.md b/README_vi.md index 83557ce5..e6d5f489 100644 --- a/README_vi.md +++ b/README_vi.md @@ -182,8 +182,8 @@ Dự án này cung cấp hai phương pháp triển khai, vui lòng chọn theo #### 🚀 Lựa chọn phương pháp triển khai | Phương pháp triển khai | Đặc điểm | Tình huống áp dụng | Tài liệu triển khai | Yêu cầu cấu hình | Video hướng dẫn | |---------|------|---------|---------|---------|---------| -| **Cài đặt tối giản** | Đối thoại thông minh, IOT, MCP, cảm nhận thị giác | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - | -| **Cài đặt toàn bộ module** | Đối thoại thông minh, IOT, điểm truy cập MCP, nhận dạng giọng nói, cảm nhận thị giác, OTA, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản Docker](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Triển khai mã nguồn](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](./docs/dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) | +| **Cài đặt tối giản** | Đối thoại thông minh, quản lý đơn tác nhân | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - | +| **Cài đặt toàn bộ module** | Đối thoại thông minh, quản lý đa người dùng, quản lý đa tác nhân, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản Docker](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Triển khai mã nguồn](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](./docs/dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) | Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](./docs/FAQ.md)