diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 92df17114f..5f7e8eb52b 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -78,6 +78,14 @@ def __init__( ) # 检查配置完整性,并插入 has_new = self.check_config_integrity(default_config, conf) + + dashboard_conf = conf.get("dashboard") + if isinstance(dashboard_conf, dict): + host_val = dashboard_conf.get("host") + if isinstance(host_val, str) and host_val: + dashboard_conf["host"] = [host_val] + has_new = True + reset_dashboard_password = self._consume_reset_dashboard_password_flag() if reset_dashboard_password and "dashboard" in conf: self._reset_generated_dashboard_password(conf) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 700a345ccc..78238dde88 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -255,7 +255,7 @@ "password_storage_upgraded": False, "password_change_required": False, "jwt_secret": "", - "host": "0.0.0.0", + "host": ["0.0.0.0", "::"], "port": 6185, "disable_access_log": True, "trust_proxy_headers": False, diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index 3e9c20e323..2e30cc006c 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -367,10 +367,32 @@ def get_local_ip_addresses(): for addr in addrs: if addr.family == socket.AF_INET: # 使用 socket.AF_INET 代替 psutil.AF_INET network_ips.append(addr.address) + elif addr.family == socket.AF_INET6: + address = addr.address + scope_idx = address.find("%") + if scope_idx != -1: + address = address[:scope_idx] + network_ips.append(address) return network_ips +def normalize_host_list(host_raw: str | list[str] | None) -> list[str]: + """Normalize a host config value into a list of host strings. + + Args: + host_raw: A string, list of strings, or None. + + Returns: + A list of non-empty host strings. + """ + if host_raw is None: + return [] + if isinstance(host_raw, list): + return [h for h in host_raw if h] + return [h.strip() for h in str(host_raw).split(",") if h.strip()] + + def get_dashboard_dist_version(dist_dir: str | Path) -> str | None: """Read the WebUI version from a dashboard dist directory. diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 6776927ba0..3b0f5207d1 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -25,6 +25,7 @@ get_dashboard_dist_version, get_local_ip_addresses, is_dashboard_dist_compatible, + normalize_host_list, should_use_bundled_dashboard_dist, ) from astrbot.dashboard.asgi_runtime import ( @@ -553,11 +554,12 @@ def run(self): or os.environ.get("ASTRBOT_DASHBOARD_PORT") or dashboard_config.get("port", 6185) ) - host = ( + host_raw = ( os.environ.get("DASHBOARD_HOST") or os.environ.get("ASTRBOT_DASHBOARD_HOST") - or dashboard_config.get("host", "0.0.0.0") + or dashboard_config.get("host", ["0.0.0.0"]) ) + hosts = normalize_host_list(host_raw) enable = dashboard_config.get("enable", True) ssl_config = dashboard_config.get("ssl", {}) if not isinstance(ssl_config, dict): @@ -578,17 +580,36 @@ def run(self): logger.info("WebUI disabled.") return None - logger.info("Starting WebUI at %s://%s:%s", scheme, host, port) - if host == "0.0.0.0": + bound_urls = [ + f"{scheme}://[{h}]:{port}" if ":" in h else f"{scheme}://{h}:{port}" + for h in hosts + ] + logger.info("Starting WebUI at %s", ", ".join(bound_urls)) + all_interfaces = {"0.0.0.0", "::"} + local_hosts = {"localhost", "127.0.0.1", "::1"} + if all_interfaces & set(hosts): logger.info( "WebUI listens on all interfaces. Check security. Set dashboard.host in data/cmd_config.json to change it.", ) - if host not in ["localhost", "127.0.0.1"]: - try: - ip_addr = get_local_ip_addresses() - except Exception as _: - pass + if not set(hosts).issubset(local_hosts): + if all_interfaces & set(hosts): + try: + ip_addr = get_local_ip_addresses() + except Exception as _: + ip_addr = [] + has_v4_wildcard = "0.0.0.0" in hosts + has_v6_wildcard = "::" in hosts + if has_v4_wildcard and not has_v6_wildcard: + specific_v6 = [h for h in hosts if ":" in h and h != "::"] + ip_addr = [ip for ip in ip_addr if ":" not in ip] + specific_v6 + elif has_v6_wildcard and not has_v4_wildcard: + specific_v4 = [h for h in hosts if ":" not in h and h != "0.0.0.0"] + ip_addr = [ip for ip in ip_addr if ":" in ip] + specific_v4 + else: + ip_addr = [h for h in hosts if h not in local_hosts] + else: + ip_addr = [] if isinstance(port, str): port = int(port) @@ -614,7 +635,10 @@ def run(self): parts = [f"\n ✨✨✨\n AstrBot v{VERSION} {webui_status}\n\n"] parts.append(f" ➜ Local: {scheme}://localhost:{port}\n") for ip in ip_addr: - parts.append(f" ➜ Network: {scheme}://{ip}:{port}\n") + if ":" in ip: + parts.append(f" ➜ Network: {scheme}://[{ip}]:{port}\n") + else: + parts.append(f" ➜ Network: {scheme}://{ip}:{port}\n") parts.append(self._build_dashboard_credentials_display()) display = "".join(parts) @@ -627,7 +651,9 @@ def run(self): # 配置 Hypercorn config = HyperConfig() - config.bind = [f"{host}:{port}"] + config.bind = [ + f"[{h}]:{port}" if ":" in h else f"{h}:{port}" for h in hosts + ] if bool(self.config.get("dashboard", {}).get("trust_proxy_headers", False)): config.logger_class = _ProxyAwareHypercornLogger if ssl_enable: diff --git a/astrbot/dashboard/services/auth_service.py b/astrbot/dashboard/services/auth_service.py index bc6cbde507..253e122830 100644 --- a/astrbot/dashboard/services/auth_service.py +++ b/astrbot/dashboard/services/auth_service.py @@ -12,6 +12,7 @@ from astrbot.core import DEMO_MODE from astrbot.core.config.astrbot_config import AstrBotConfig from astrbot.core.db import BaseDatabase +from astrbot.core.utils.io import normalize_host_list from astrbot.core.utils.auth_password import ( is_default_dashboard_password, is_md5_dashboard_password, @@ -427,12 +428,17 @@ async def is_setup_required(self) -> bool: def can_skip_default_password_auth(self) -> bool: if not self.env_flag_enabled(SKIP_DEFAULT_PASSWORD_AUTH_ENV): return False - host = ( + host_raw = ( os.environ.get("DASHBOARD_HOST") or os.environ.get("ASTRBOT_DASHBOARD_HOST") or self.config["dashboard"].get("host", "") ) - return str(host).strip().lower() in LOCAL_DASHBOARD_HOSTS + hosts = normalize_host_list(host_raw) + if not hosts: + return False + return all( + str(h).strip().lower() in LOCAL_DASHBOARD_HOSTS for h in hosts + ) @staticmethod def env_flag_enabled(name: str) -> bool: