From 468cee23ecd094c1a036ef636ddb77d6ee17745c Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Fri, 27 Feb 2026 22:02:14 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"=E5=8F=AF=E9=80=89=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=89=8D=E5=90=8E=E7=AB=AF=E5=88=86?= =?UTF-8?q?=E7=A6=BB"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .python-version | 2 +- astrbot/cli/commands/cmd_init.py | 9 +- astrbot/cli/commands/cmd_run.py | 20 +- astrbot/core/config/default.py | 2 +- .../aiocqhttp/aiocqhttp_platform_adapter.py | 2 +- .../qqofficial_webhook/qo_webhook_server.py | 2 +- .../platform/sources/wecom/wecom_adapter.py | 4 +- astrbot/core/utils/io.py | 47 +- astrbot/dashboard/routes/__init__.py | 8 - astrbot/dashboard/routes/route.py | 6 +- astrbot/dashboard/routes/static_file.py | 3 - astrbot/dashboard/server.py | 442 +++---- dashboard/env.d.ts | 6 - dashboard/public/config.json | 13 - dashboard/src/components/chat/LiveMode.vue | 1070 ++++++++-------- .../src/i18n/locales/en-US/features/auth.json | 13 +- .../i18n/locales/en-US/features/settings.json | 8 - .../src/i18n/locales/zh-CN/features/auth.json | 13 +- .../i18n/locales/zh-CN/features/settings.json | 8 - dashboard/src/main.ts | 265 ++-- dashboard/src/stores/api.ts | 70 -- dashboard/src/views/Settings.vue | 1088 +++++++---------- .../views/authentication/auth/LoginPage.vue | 219 +--- dashboard/tsconfig.json | 26 +- dashboard/tsconfig.vite-config.json | 8 +- dashboard/vite.config.ts | 48 +- pyproject.toml | 3 +- 27 files changed, 1255 insertions(+), 2150 deletions(-) delete mode 100644 dashboard/public/config.json delete mode 100644 dashboard/src/stores/api.ts diff --git a/.python-version b/.python-version index e4fba21835..fdcfcfdfca 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12 +3.12 \ No newline at end of file diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index 0adbf32884..6c0c34b99c 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -34,13 +34,8 @@ async def initialize_astrbot(astrbot_root: Path) -> None: for name, path in paths.items(): path.mkdir(parents=True, exist_ok=True) click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}") - if click.confirm( - "是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)", - default=True, - ): - await check_dashboard(astrbot_root / "data") - else: - click.echo("你可以使用在线面版(v4.14.4+),填写后端地址的方式来控制。") + + await check_dashboard(astrbot_root / "data") @click.command() diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index 3641d31c46..23665dff3d 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -15,8 +15,7 @@ async def run_astrbot(astrbot_root: Path) -> None: from astrbot.core import LogBroker, LogManager, db_helper, logger from astrbot.core.initial_loader import InitialLoader - if os.environ.get("DASHBOARD_ENABLE") == "True": - await check_dashboard(astrbot_root / "data") + await check_dashboard(astrbot_root / "data") log_broker = LogBroker() LogManager.set_queue_handler(logger, log_broker) @@ -28,17 +27,9 @@ async def run_astrbot(astrbot_root: Path) -> None: @click.option("--reload", "-r", is_flag=True, help="插件自动重载") -@click.option( - "--host", "-H", help="Astrbot Dashboard Host,默认::", required=False, type=str -) -@click.option( - "--port", "-p", help="Astrbot Dashboard端口,默认6185", required=False, type=str -) -@click.option( - "--backend-only", is_flag=True, default=False, help="禁用WEBUI,仅启动后端" -) +@click.option("--port", "-p", help="Astrbot Dashboard端口", required=False, type=str) @click.command() -def run(reload: bool, host: str, port: str, backend_only: bool) -> None: +def run(reload: bool, port: str) -> None: """运行 AstrBot""" try: os.environ["ASTRBOT_CLI"] = "1" @@ -52,11 +43,8 @@ def run(reload: bool, host: str, port: str, backend_only: bool) -> None: os.environ["ASTRBOT_ROOT"] = str(astrbot_root) sys.path.insert(0, str(astrbot_root)) - if port is not None: + if port: os.environ["DASHBOARD_PORT"] = port - if host is not None: - os.environ["DASHBOARD_HOST"] = host - os.environ["DASHBOARD_ENABLE"] = str(not backend_only) if reload: click.echo("启用插件自动重载") diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index af2829733c..fa9d71d745 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -195,7 +195,7 @@ "username": "astrbot", "password": "77b90590a8945a7d36c963981a307dc9", "jwt_secret": "", - "host": "::", + "host": "0.0.0.0", "port": 6185, "disable_access_log": True, "ssl": { diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 2f720dd1c6..45114382fa 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -419,7 +419,7 @@ async def _convert_handle_message_event( def run(self) -> Awaitable[Any]: if not self.host or not self.port: logger.warning( - "aiocqhttp: 未配置 ws_reverse_host 或 ws_reverse_port,将使用默认值:http://[::]:6199", + "aiocqhttp: 未配置 ws_reverse_host 或 ws_reverse_port,将使用默认值:http://0.0.0.0:6199", ) self.host = "0.0.0.0" self.port = 6199 diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py index e1c5d457aa..5f35471eea 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py @@ -21,7 +21,7 @@ def __init__( self.secret = config["secret"] self.port = config.get("port", 6196) self.is_sandbox = config.get("is_sandbox", False) - self.callback_server_host = config.get("callback_server_host", "::") + self.callback_server_host = config.get("callback_server_host", "0.0.0.0") if isinstance(self.port, str): self.port = int(self.port) diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index c73e15a08b..6647db89f0 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -43,7 +43,7 @@ class WecomServer: def __init__(self, event_queue: asyncio.Queue, config: dict) -> None: self.server = quart.Quart(__name__) self.port = int(cast(str, config.get("port"))) - self.callback_server_host = config.get("callback_server_host", "::") + self.callback_server_host = config.get("callback_server_host", "0.0.0.0") self.server.add_url_rule( "/callback/command", view_func=self.verify, @@ -407,7 +407,7 @@ async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None: abm.message = [Image(file=path, url=path)] elif msgtype == "voice": media_id = msg.get("voice", {}).get("media_id", "") - resp = await asyncio.get_event_loop().run_in_executor( + resp: Response = await asyncio.get_event_loop().run_in_executor( None, self.client.media.download, media_id, diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index 911224dfe9..0ce3624e81 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -1,4 +1,3 @@ -import asyncio import base64 import logging import os @@ -8,7 +7,6 @@ import time import uuid import zipfile -from ipaddress import IPv4Address, IPv6Address, ip_address from pathlib import Path import aiohttp @@ -208,53 +206,18 @@ def file_to_base64(file_path: str) -> str: return "base64://" + base64_str -def get_local_ip_addresses() -> list[IPv4Address | IPv6Address]: +def get_local_ip_addresses(): net_interfaces = psutil.net_if_addrs() - network_ips: list[IPv4Address | IPv6Address] = [] + network_ips = [] - for _, addrs in net_interfaces.items(): + for interface, addrs in net_interfaces.items(): for addr in addrs: - if addr.family == socket.AF_INET: - network_ips.append(ip_address(addr.address)) - elif addr.family == socket.AF_INET6: - # 过滤掉 IPv6 的 link-local 地址(fe80:...) - ip = ip_address(addr.address.split("%")[0]) # 处理带 zone index 的情况 - if not ip.is_link_local: - network_ips.append(ip) + if addr.family == socket.AF_INET: # 使用 socket.AF_INET 代替 psutil.AF_INET + network_ips.append(addr.address) return network_ips -async def get_public_ip_address() -> list[IPv4Address | IPv6Address]: - urls = [ - "https://api64.ipify.org", - "https://ident.me", - "https://ifconfig.me", - "https://icanhazip.com", - ] - found_ips: dict[int, IPv4Address | IPv6Address] = {} - - async def fetch(session: aiohttp.ClientSession, url: str): - try: - async with session.get(url, timeout=3) as resp: - if resp.status == 200: - raw_ip = (await resp.text()).strip() - ip = ip_address(raw_ip) - if ip.version not in found_ips: - found_ips[ip.version] = ip - except Exception as e: - # Ignore errors from individual services so that a single failing - # endpoint does not prevent discovering the public IP from others. - logger.debug("Failed to fetch public IP from %s: %s", url, e) - - async with aiohttp.ClientSession() as session: - tasks = [fetch(session, url) for url in urls] - await asyncio.gather(*tasks) - - # 返回找到的所有 IP 对象列表 - return list(found_ips.values()) - - async def get_dashboard_version(): dist_dir = os.path.join(get_astrbot_data_path(), "dist") if os.path.exists(dist_dir): diff --git a/astrbot/dashboard/routes/__init__.py b/astrbot/dashboard/routes/__init__.py index 652a9feef0..fbbd0c7a08 100644 --- a/astrbot/dashboard/routes/__init__.py +++ b/astrbot/dashboard/routes/__init__.py @@ -9,20 +9,16 @@ from .cron import CronRoute from .file import FileRoute from .knowledge_base import KnowledgeBaseRoute -from .live_chat import LiveChatRoute from .log import LogRoute from .open_api import OpenApiRoute from .persona import PersonaRoute from .platform import PlatformRoute from .plugin import PluginRoute -from .response import Response -from .route import RouteContext from .session_management import SessionManagementRoute from .skills import SkillsRoute from .stat import StatRoute from .static_file import StaticFileRoute from .subagent import SubAgentRoute -from .t2i import T2iRoute from .tools import ToolsRoute from .update import UpdateRoute @@ -50,8 +46,4 @@ "ToolsRoute", "SkillsRoute", "UpdateRoute", - "T2iRoute", - "LiveChatRoute", - "Response", - "RouteContext", ] diff --git a/astrbot/dashboard/routes/route.py b/astrbot/dashboard/routes/route.py index 4fdc37971a..53c6234439 100644 --- a/astrbot/dashboard/routes/route.py +++ b/astrbot/dashboard/routes/route.py @@ -1,4 +1,4 @@ -from dataclasses import asdict, dataclass +from dataclasses import dataclass from quart import Quart @@ -57,7 +57,3 @@ def ok(self, data: dict | list | None = None, message: str | None = None): self.data = data self.message = message return self - - def to_json(self): - # Return a plain dict so callers can safely wrap with jsonify() - return asdict(self) diff --git a/astrbot/dashboard/routes/static_file.py b/astrbot/dashboard/routes/static_file.py index 15fec95d1c..e056b6c5ac 100644 --- a/astrbot/dashboard/routes/static_file.py +++ b/astrbot/dashboard/routes/static_file.py @@ -5,9 +5,6 @@ class StaticFileRoute(Route): def __init__(self, context: RouteContext) -> None: super().__init__(context) - if "index" in self.app.view_functions: - return - index_ = [ "/", "/auth/login", diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index e7fab87425..a9650cd06b 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -2,12 +2,9 @@ import hashlib import logging import os -import platform import socket -from collections.abc import Callable -from ipaddress import IPv4Address, IPv6Address, ip_address from pathlib import Path -from typing import Protocol +from typing import Protocol, cast import jwt import psutil @@ -16,7 +13,6 @@ from hypercorn.config import Config as HyperConfig from quart import Quart, g, jsonify, request from quart.logging import default_handler -from quart_cors import cors from astrbot.core import logger from astrbot.core.config.default import VERSION @@ -27,6 +23,13 @@ from .routes import * from .routes.api_key import ALL_OPEN_API_SCOPES +from .routes.backup import BackupRoute +from .routes.live_chat import LiveChatRoute +from .routes.platform import PlatformRoute +from .routes.route import Response, RouteContext +from .routes.session_management import SessionManagementRoute +from .routes.subagent import SubAgentRoute +from .routes.t2i import T2iRoute class _AddrWithPort(Protocol): @@ -43,16 +46,6 @@ def _parse_env_bool(value: str | None, default: bool) -> bool: class AstrBotDashboard: - """AstrBot Web Dashboard""" - - ALLOWED_ENDPOINT_PREFIXES = ( - "/api/auth/login", - "/api/file", - "/api/platform/webhook", - "/api/stat/start-time", - "/api/backup/download", - ) - def __init__( self, core_lifecycle: AstrBotCoreLifecycle, @@ -63,123 +56,67 @@ def __init__( self.core_lifecycle = core_lifecycle self.config = core_lifecycle.astrbot_config self.db = db - self.shutdown_event = shutdown_event - - self.enable_webui = self._check_webui_enabled() - - self._init_paths(webui_dir) - self._init_app() - self.context = RouteContext(self.config, self.app) - - self._init_routes(db) - self._init_plugin_route_index() - self._init_jwt_secret() - - # ------------------------------------------------------------------ - # 初始化阶段 - # ------------------------------------------------------------------ - - def _check_webui_enabled(self) -> bool: - cfg = self.config.get("dashboard", {}) - _env = os.environ.get("DASHBOARD_ENABLE") - if _env is not None: - return _env.lower() in ("true", "1", "yes") - return cfg.get("enable", True) - def _init_paths(self, webui_dir: str | None): + # 参数指定webui目录 if webui_dir and os.path.exists(webui_dir): self.data_path = os.path.abspath(webui_dir) else: self.data_path = os.path.abspath( - os.path.join(get_astrbot_data_path(), "dist") + os.path.join(get_astrbot_data_path(), "dist"), ) - def _init_app(self): - """初始化 Quart 应用""" - global APP - self.app = Quart( - "AstrBotDashboard", - static_folder=self.data_path, - static_url_path="/", - ) - APP = self.app - self.app.json_provider_class = DefaultJSONProvider - self.app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16MB - - # 配置 CORS - self.app = cors( - self.app, - allow_origin="*", - allow_headers=["Authorization", "Content-Type", "X-API-Key"], - allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], - ) - - @self.app.route("/") - async def index(): - if not self.enable_webui: - return "WebUI is disabled." - return await self.app.send_static_file("index.html") - - @self.app.errorhandler(404) - async def not_found(e): - if not self.enable_webui: - return "WebUI is disabled." - if request.path.startswith("/api/"): - return jsonify(Response().error("Not Found").to_json()), 404 - return await self.app.send_static_file("index.html") - - @self.app.before_serving - async def startup(): - pass - - @self.app.after_serving - async def shutdown(): - pass - + self.app = Quart("dashboard", static_folder=self.data_path, static_url_path="/") + APP = self.app # noqa + self.app.config["MAX_CONTENT_LENGTH"] = ( + 128 * 1024 * 1024 + ) # 将 Flask 允许的最大上传文件体大小设置为 128 MB + cast(DefaultJSONProvider, self.app.json).sort_keys = False self.app.before_request(self.auth_middleware) + # token 用于验证请求 logging.getLogger(self.app.name).removeHandler(default_handler) - - def _init_routes(self, db: BaseDatabase): - UpdateRoute( - self.context, self.core_lifecycle.astrbot_updator, self.core_lifecycle + self.context = RouteContext(self.config, self.app) + self.ur = UpdateRoute( + self.context, + core_lifecycle.astrbot_updator, + core_lifecycle, ) - StatRoute(self.context, db, self.core_lifecycle) - PluginRoute( - self.context, self.core_lifecycle, self.core_lifecycle.plugin_manager + self.sr = StatRoute(self.context, db, core_lifecycle) + self.pr = PluginRoute( + self.context, + core_lifecycle, + core_lifecycle.plugin_manager, ) self.command_route = CommandRoute(self.context) - self.cr = ConfigRoute(self.context, self.core_lifecycle) - self.lr = LogRoute(self.context, self.core_lifecycle.log_broker) + self.cr = ConfigRoute(self.context, core_lifecycle) + self.lr = LogRoute(self.context, core_lifecycle.log_broker) self.sfr = StaticFileRoute(self.context) self.ar = AuthRoute(self.context) self.api_key_route = ApiKeyRoute(self.context, db) - self.chat_route = ChatRoute(self.context, db, self.core_lifecycle) + self.chat_route = ChatRoute(self.context, db, core_lifecycle) self.open_api_route = OpenApiRoute( self.context, db, - self.core_lifecycle, + core_lifecycle, self.chat_route, ) self.chatui_project_route = ChatUIProjectRoute(self.context, db) - self.tools_root = ToolsRoute(self.context, self.core_lifecycle) - self.subagent_route = SubAgentRoute(self.context, self.core_lifecycle) - self.skills_route = SkillsRoute(self.context, self.core_lifecycle) - self.conversation_route = ConversationRoute( - self.context, db, self.core_lifecycle - ) + self.tools_root = ToolsRoute(self.context, core_lifecycle) + self.subagent_route = SubAgentRoute(self.context, core_lifecycle) + self.skills_route = SkillsRoute(self.context, core_lifecycle) + self.conversation_route = ConversationRoute(self.context, db, core_lifecycle) self.file_route = FileRoute(self.context) self.session_management_route = SessionManagementRoute( self.context, db, - self.core_lifecycle, + core_lifecycle, ) - self.persona_route = PersonaRoute(self.context, db, self.core_lifecycle) - self.cron_route = CronRoute(self.context, self.core_lifecycle) - self.t2i_route = T2iRoute(self.context, self.core_lifecycle) - self.kb_route = KnowledgeBaseRoute(self.context, self.core_lifecycle) - self.platform_route = PlatformRoute(self.context, self.core_lifecycle) - self.backup_route = BackupRoute(self.context, db, self.core_lifecycle) - self.live_chat_route = LiveChatRoute(self.context, db, self.core_lifecycle) + self.persona_route = PersonaRoute(self.context, db, core_lifecycle) + self.cron_route = CronRoute(self.context, core_lifecycle) + self.t2i_route = T2iRoute(self.context, core_lifecycle) + self.kb_route = KnowledgeBaseRoute(self.context, core_lifecycle) + self.platform_route = PlatformRoute(self.context, core_lifecycle) + self.backup_route = BackupRoute(self.context, db, core_lifecycle) + self.live_chat_route = LiveChatRoute(self.context, db, core_lifecycle) self.app.add_url_rule( "/api/plug/", @@ -187,35 +124,20 @@ def _init_routes(self, db: BaseDatabase): methods=["GET", "POST"], ) - def _init_plugin_route_index(self): - """将插件路由索引,避免 O(n) 查找""" - self._plugin_route_map: dict[tuple[str, str], Callable] = {} - - for ( - route, - handler, - methods, - _, - ) in self.core_lifecycle.star_context.registered_web_apis: - for method in methods: - self._plugin_route_map[(route, method)] = handler - - def _init_jwt_secret(self): - dashboard_cfg = self.config.setdefault("dashboard", {}) - if not dashboard_cfg.get("jwt_secret"): - dashboard_cfg["jwt_secret"] = os.urandom(32).hex() - self.config.save_config() - logger.info("Initialized random JWT secret for dashboard.") - self._jwt_secret = dashboard_cfg["jwt_secret"] + self.shutdown_event = shutdown_event + + self._init_jwt_secret() - # ------------------------------------------------------------------ - # Middleware中间件 - # ------------------------------------------------------------------ + async def srv_plug_route(self, subpath, *args, **kwargs): + """插件路由""" + registered_web_apis = self.core_lifecycle.star_context.registered_web_apis + for api in registered_web_apis: + route, view_handler, methods, _ = api + if route == f"/{subpath}" and request.method in methods: + return await view_handler(*args, **kwargs) + return jsonify(Response().error("未找到该路由").__dict__) async def auth_middleware(self): - # 放行CORS预检请求 - if request.method == "OPTIONS": - return None if not request.path.startswith("/api"): return None if request.path.startswith("/api/v1"): @@ -252,46 +174,33 @@ async def auth_middleware(self): await self.db.touch_api_key(api_key.key_id) return None - if any(request.path.startswith(p) for p in self.ALLOWED_ENDPOINT_PREFIXES): + allowed_endpoints = [ + "/api/auth/login", + "/api/file", + "/api/platform/webhook", + "/api/stat/start-time", + "/api/backup/download", # 备份下载使用 URL 参数传递 token + ] + if any(request.path.startswith(prefix) for prefix in allowed_endpoints): return None - + # 声明 JWT token = request.headers.get("Authorization") if not token: - return self._unauthorized("未授权") - + r = jsonify(Response().error("未授权").__dict__) + r.status_code = 401 + return r + token = token.removeprefix("Bearer ") try: - payload = jwt.decode( - token.removeprefix("Bearer "), - self._jwt_secret, - algorithms=["HS256"], - options={"require": ["username"]}, - ) + payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"]) g.username = payload["username"] except jwt.ExpiredSignatureError: - return self._unauthorized("Token 过期") - except jwt.PyJWTError: - return self._unauthorized("Token 无效") - - @staticmethod - def _unauthorized(msg: str): - r = jsonify(Response().error(msg).to_json()) - r.status_code = 401 - return r - - # ------------------------------------------------------------------ - # 插件路由 - # ------------------------------------------------------------------ - - async def srv_plug_route(self, subpath: str, *args, **kwargs): - handler = self._plugin_route_map.get((f"/{subpath}", request.method)) - if not handler: - return jsonify(Response().error("未找到该路由").to_json()) - - try: - return await handler(*args, **kwargs) - except Exception: - logger.exception("插件 Web API 执行异常") - return jsonify(Response().error("插件 Web API 执行异常").to_json()) + r = jsonify(Response().error("Token 过期").__dict__) + r.status_code = 401 + return r + except jwt.InvalidTokenError: + r = jsonify(Response().error("Token 无效").__dict__) + r.status_code = 401 + return r @staticmethod def _extract_raw_api_key() -> str | None: @@ -321,87 +230,126 @@ def _get_required_open_api_scope(path: str) -> str | None: } return scope_map.get(path) - def check_port_in_use(self, host: str, port: int) -> bool: + def check_port_in_use(self, port: int) -> bool: """跨平台检测端口是否被占用""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - try: - s.bind((host, port)) - return False - except OSError: - return True + try: + # 创建 IPv4 TCP Socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # 设置超时时间 + sock.settimeout(2) + result = sock.connect_ex(("127.0.0.1", port)) + sock.close() + # result 为 0 表示端口被占用 + return result == 0 + except Exception as e: + logger.warning(f"检查端口 {port} 时发生错误: {e!s}") + # 如果出现异常,保守起见认为端口可能被占用 + return True def get_process_using_port(self, port: int) -> str: - """获取占用端口的进程信息""" + """获取占用端口的进程详细信息""" try: - for proc in psutil.process_iter(["pid", "name", "connections"]): - for conn in proc.info["connections"] or []: # type: ignore - if conn.laddr.port == port: - return f"PID: {proc.info['pid']}, Name: {proc.info['name']}" # type: ignore + for conn in psutil.net_connections(kind="inet"): + if cast(_AddrWithPort, conn.laddr).port == port: + try: + process = psutil.Process(conn.pid) + # 获取详细信息 + proc_info = [ + f"进程名: {process.name()}", + f"PID: {process.pid}", + f"执行路径: {process.exe()}", + f"工作目录: {process.cwd()}", + f"启动命令: {' '.join(process.cmdline())}", + ] + return "\n ".join(proc_info) + except (psutil.NoSuchProcess, psutil.AccessDenied) as e: + return f"无法获取进程详细信息(可能需要管理员权限): {e!s}" + return "未找到占用进程" except Exception as e: return f"获取进程信息失败: {e!s}" - return "未知进程" - - # ------------------------------------------------------------------ - # 启动与运行 - # ------------------------------------------------------------------ - - def run(self) -> None: - """Run dashboard server (blocking)""" - if not self.enable_webui: - logger.warning( - "WebUI 已禁用 (dashboard.enable=false or DASHBOARD_ENABLE=false)" - ) - dashboard_config = self.config.get("dashboard", {}) - host = os.environ.get("DASHBOARD_HOST") or dashboard_config.get( - "host", "0.0.0.0" + def _init_jwt_secret(self) -> None: + if not self.config.get("dashboard", {}).get("jwt_secret", None): + # 如果没有设置 JWT 密钥,则生成一个新的密钥 + jwt_secret = os.urandom(32).hex() + self.config["dashboard"]["jwt_secret"] = jwt_secret + self.config.save_config() + logger.info("Initialized random JWT secret for dashboard.") + self._jwt_secret = self.config["dashboard"]["jwt_secret"] + + def run(self): + ip_addr = [] + dashboard_config = self.core_lifecycle.astrbot_config.get("dashboard", {}) + port = ( + os.environ.get("DASHBOARD_PORT") + or os.environ.get("ASTRBOT_DASHBOARD_PORT") + or dashboard_config.get("port", 6185) ) - port = int( - os.environ.get("DASHBOARD_PORT") or dashboard_config.get("port", 6185) + host = ( + os.environ.get("DASHBOARD_HOST") + or os.environ.get("ASTRBOT_DASHBOARD_HOST") + or dashboard_config.get("host", "0.0.0.0") ) + enable = dashboard_config.get("enable", True) ssl_config = dashboard_config.get("ssl", {}) + if not isinstance(ssl_config, dict): + ssl_config = {} ssl_enable = _parse_env_bool( - os.environ.get("DASHBOARD_SSL_ENABLE"), - ssl_config.get("enable", False), + os.environ.get("DASHBOARD_SSL_ENABLE") + or os.environ.get("ASTRBOT_DASHBOARD_SSL_ENABLE"), + bool(ssl_config.get("enable", False)), ) - scheme = "https" if ssl_enable else "http" - display_host = f"[{host}]" if ":" in host else host - if self.enable_webui: + if not enable: + logger.info("WebUI 已被禁用") + return None + + logger.info(f"正在启动 WebUI, 监听地址: {scheme}://{host}:{port}") + if host == "0.0.0.0": logger.info( - "正在启动 WebUI + API, 监听地址: %s://%s:%s", - scheme, - display_host, - port, + "提示: WebUI 将监听所有网络接口,请注意安全。(可在 data/cmd_config.json 中配置 dashboard.host 以修改 host)", ) - else: - logger.info( - "正在启动 API Server (WebUI 已分离), 监听地址: %s://%s:%s", - scheme, - display_host, - port, + + if host not in ["localhost", "127.0.0.1"]: + try: + ip_addr = get_local_ip_addresses() + except Exception as _: + pass + if isinstance(port, str): + port = int(port) + + if self.check_port_in_use(port): + process_info = self.get_process_using_port(port) + logger.error( + f"错误:端口 {port} 已被占用\n" + f"占用信息: \n {process_info}\n" + f"请确保:\n" + f"1. 没有其他 AstrBot 实例正在运行\n" + f"2. 端口 {port} 没有被其他程序占用\n" + f"3. 如需使用其他端口,请修改配置文件", ) - check_hosts = {host} - if host not in ("127.0.0.1", "localhost", "::1"): - check_hosts.add("127.0.0.1") - for check_host in check_hosts: - if self.check_port_in_use(check_host, port): - info = self.get_process_using_port(port) - raise RuntimeError(f"端口 {port} 已被占用\n{info}") + raise Exception(f"端口 {port} 已被占用") + + parts = [f"\n ✨✨✨\n AstrBot v{VERSION} WebUI 已启动,可访问\n\n"] + parts.append(f" ➜ 本地: {scheme}://localhost:{port}\n") + for ip in ip_addr: + parts.append(f" ➜ 网络: {scheme}://{ip}:{port}\n") + parts.append(" ➜ 默认用户名和密码: astrbot\n ✨✨✨\n") + display = "".join(parts) + + if not ip_addr: + display += ( + "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" + ) - if self.enable_webui: - self._print_access_urls(host, port, scheme) + logger.info(display) # 配置 Hypercorn config = HyperConfig() - binds: list[str] = [self._build_bind(host, port)] - # 参考:https://github.com/pgjones/hypercorn/issues/85 - if host == "::" and platform.system() in ("Windows", "Darwin"): - binds.append(self._build_bind("0.0.0.0", port)) - config.bind = binds - + config.bind = [f"{host}:{port}"] if ssl_enable: cert_file = ( os.environ.get("DASHBOARD_SSL_CERT") @@ -444,48 +392,12 @@ def run(self) -> None: if disable_access_log: config.accesslog = None else: + # 启用访问日志,使用简洁格式 config.accesslog = "-" config.access_log_format = "%(h)s %(r)s %(s)s %(b)s %(D)s" - return asyncio.run( - serve(self.app, config, shutdown_trigger=self.shutdown_trigger) - ) - - @staticmethod - def _build_bind(host: str, port: int) -> str: - try: - ip: IPv4Address | IPv6Address = ip_address(host) - return f"[{ip}]:{port}" if ip.version == 6 else f"{ip}:{port}" - except ValueError: - return f"{host}:{port}" - - def _print_access_urls(self, host: str, port: int, scheme: str = "http") -> None: - local_ips: list[IPv4Address | IPv6Address] = get_local_ip_addresses() - - parts = [f"\n ✨✨✨\n AstrBot v{VERSION} WebUI 已启动\n\n"] - - parts.append(f" ➜ 本地: {scheme}://localhost:{port}\n") - - if host in ("::", "0.0.0.0"): - for ip in local_ips: - if ip.is_loopback: - continue - - if ip.version == 6: - display_url = f"{scheme}://[{ip}]:{port}" - else: - display_url = f"{scheme}://{ip}:{port}" - - parts.append(f" ➜ 网络: {display_url}\n") - - parts.append(" ➜ 默认用户名和密码: astrbot\n ✨✨✨\n") - - if not local_ips: - parts.append( - "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" - ) - - logger.info("".join(parts)) + return serve(self.app, config, shutdown_trigger=self.shutdown_trigger) - async def shutdown_trigger(self): + async def shutdown_trigger(self) -> None: await self.shutdown_event.wait() + logger.info("AstrBot WebUI 已经被优雅地关闭") diff --git a/dashboard/env.d.ts b/dashboard/env.d.ts index a90bd47be0..b4b3508300 100644 --- a/dashboard/env.d.ts +++ b/dashboard/env.d.ts @@ -7,9 +7,3 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; } - -declare module "*.vue" { - import type { DefineComponent } from "vue"; - const component: DefineComponent<{}, {}, any>; - export default component; -} diff --git a/dashboard/public/config.json b/dashboard/public/config.json deleted file mode 100644 index 0d7e84a8ad..0000000000 --- a/dashboard/public/config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "apiBaseUrl": "", - "presets": [ - { - "name": "Default (Auto)", - "url": "" - }, - { - "name": "Localhost", - "url": "http://localhost:6185" - } - ] -} diff --git a/dashboard/src/components/chat/LiveMode.vue b/dashboard/src/components/chat/LiveMode.vue index 2e11277adb..2740459d96 100644 --- a/dashboard/src/components/chat/LiveMode.vue +++ b/dashboard/src/components/chat/LiveMode.vue @@ -1,110 +1,65 @@ diff --git a/dashboard/src/i18n/locales/en-US/features/auth.json b/dashboard/src/i18n/locales/en-US/features/auth.json index c59deb2a0d..5c44558a03 100644 --- a/dashboard/src/i18n/locales/en-US/features/auth.json +++ b/dashboard/src/i18n/locales/en-US/features/auth.json @@ -10,16 +10,5 @@ "theme": { "switchToDark": "Switch to Dark Theme", "switchToLight": "Switch to Light Theme" - }, - "serverConfig": { - "title": "Server Configuration", - "description": "If the backend is not on the same origin (host/port), please specify the full URL here.", - "label": "API Base URL", - "placeholder": "e.g. http://localhost:6185", - "hint": "Empty for default (relative path)", - "presetLabel": "Quick Select Preset", - "save": "Save & Reload", - "cancel": "Cancel", - "tooltip": "Server Configuration" } -} +} \ No newline at end of file diff --git a/dashboard/src/i18n/locales/en-US/features/settings.json b/dashboard/src/i18n/locales/en-US/features/settings.json index 0c616c3d00..19232125f9 100644 --- a/dashboard/src/i18n/locales/en-US/features/settings.json +++ b/dashboard/src/i18n/locales/en-US/features/settings.json @@ -1,14 +1,6 @@ { "network": { "title": "Network", - "server": { - "title": "Server Address", - "subtitle": "Configure backend API URL", - "label": "API Base URL", - "placeholder": "e.g. http://localhost:6185", - "hint": "Empty for default (relative path)", - "save": "Save & Reload" - }, "githubProxy": { "title": "GitHub Proxy Address", "subtitle": "Set the GitHub proxy address used when downloading plugins or updating AstrBot. This is effective in mainland China's network environment. Can be customized, input takes effect in real time. All addresses do not guarantee stability. If errors occur when updating plugins/projects, please first check if the proxy address is working properly.", diff --git a/dashboard/src/i18n/locales/zh-CN/features/auth.json b/dashboard/src/i18n/locales/zh-CN/features/auth.json index 4318eca953..d6da999430 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/auth.json +++ b/dashboard/src/i18n/locales/zh-CN/features/auth.json @@ -10,16 +10,5 @@ "theme": { "switchToDark": "切换到深色主题", "switchToLight": "切换到浅色主题" - }, - "serverConfig": { - "title": "服务器配置", - "description": "如果后端服务不在同源(主机/端口不同),请在此指定完整 URL。", - "label": "API 基础地址", - "placeholder": "例如:http://localhost:6185", - "hint": "留空以使用默认设置(相对路径)", - "presetLabel": "快速选择预设", - "save": "保存并刷新", - "cancel": "取消", - "tooltip": "服务器配置" } -} +} \ No newline at end of file diff --git a/dashboard/src/i18n/locales/zh-CN/features/settings.json b/dashboard/src/i18n/locales/zh-CN/features/settings.json index f9a703f7e0..19c1c7c41e 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/settings.json +++ b/dashboard/src/i18n/locales/zh-CN/features/settings.json @@ -1,14 +1,6 @@ { "network": { "title": "网络", - "server": { - "title": "服务器地址", - "subtitle": "配置后端 API 地址", - "label": "API 基础地址", - "placeholder": "例如:http://localhost:6185", - "hint": "留空以使用默认设置(相对路径)", - "save": "保存并刷新" - }, "githubProxy": { "title": "GitHub 加速地址", "subtitle": "设置下载插件或者更新 AstrBot 时所用的 GitHub 加速地址。这在中国大陆的网络环境有效。可以自定义,输入结果实时生效。所有地址均不保证稳定性,如果在更新插件/项目时出现报错,请首先检查加速地址是否能正常使用。", diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts index 08a5aeadd2..6871666546 100644 --- a/dashboard/src/main.ts +++ b/dashboard/src/main.ts @@ -1,181 +1,116 @@ -import { createApp } from "vue"; -import { createPinia } from "pinia"; -import App from "./App.vue"; -import { router } from "./router"; -import vuetify from "./plugins/vuetify"; -import confirmPlugin from "./plugins/confirmPlugin"; -import { setupI18n } from "./i18n/composables"; -import "@/scss/style.scss"; -import VueApexCharts from "vue3-apexcharts"; - -import print from "vue3-print-nb"; -import { loader } from "@guolao/vue-monaco-editor"; -import axios from "axios"; - -// 1. 定义加载配置的函数 -async function loadAppConfig() { - try { - // 加上时间戳防止浏览器缓存 config.json - const response = await fetch(`/config.json?t=${new Date().getTime()}`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.warn("Failed to load config.json, falling back to default.", error); - return {}; - } -} - -function mountApp(app: any, pinia: any) { - app.mount("#app"); - +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import App from './App.vue'; +import { router } from './router'; +import vuetify from './plugins/vuetify'; +import confirmPlugin from './plugins/confirmPlugin'; +import { setupI18n } from './i18n/composables'; +import '@/scss/style.scss'; +import VueApexCharts from 'vue3-apexcharts'; + +import print from 'vue3-print-nb'; +import { loader } from '@guolao/vue-monaco-editor' +import axios from 'axios'; + +// 初始化新的i18n系统,等待完成后再挂载应用 +setupI18n().then(() => { + console.log('🌍 新i18n系统初始化完成'); + + const app = createApp(App); + app.use(router); + const pinia = createPinia(); + app.use(pinia); + app.use(print); + app.use(VueApexCharts); + app.use(vuetify); + app.use(confirmPlugin); + app.mount('#app'); + // 挂载后同步 Vuetify 主题 - import("./stores/customizer").then(({ useCustomizerStore }) => { + import('./stores/customizer').then(({ useCustomizerStore }) => { const customizer = useCustomizerStore(pinia); vuetify.theme.global.name.value = customizer.uiTheme; - const storedPrimary = localStorage.getItem("themePrimary"); - const storedSecondary = localStorage.getItem("themeSecondary"); + const storedPrimary = localStorage.getItem('themePrimary'); + const storedSecondary = localStorage.getItem('themeSecondary'); if (storedPrimary || storedSecondary) { const themes = vuetify.theme.themes.value; - ["PurpleTheme", "PurpleThemeDark"].forEach((name) => { + ['PurpleTheme', 'PurpleThemeDark'].forEach((name) => { const theme = themes[name]; if (!theme?.colors) return; if (storedPrimary) theme.colors.primary = storedPrimary; if (storedSecondary) theme.colors.secondary = storedSecondary; - if (storedPrimary && theme.colors.darkprimary) - theme.colors.darkprimary = storedPrimary; - if (storedSecondary && theme.colors.darksecondary) - theme.colors.darksecondary = storedSecondary; + if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary; + if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary; }); } }); -} - -async function initApp() { - // 等待配置加载 - const config = await loadAppConfig(); - const configApiUrl = config.apiBaseUrl || ""; - const presets = config.presets || []; - - // 优先使用 localStorage 中的配置,其次是 config.json,最后是空字符串 - const localApiUrl = localStorage.getItem("apiBaseUrl"); - const apiBaseUrl = localApiUrl !== null ? localApiUrl : configApiUrl; - - if (apiBaseUrl) { - console.log( - `API Base URL set to: ${apiBaseUrl} (Local: ${localApiUrl}, Config: ${configApiUrl})`, - ); - } - - // 配置 Axios 全局 Base URL - axios.defaults.baseURL = apiBaseUrl; - - axios.interceptors.request.use((config) => { - const token = localStorage.getItem("token"); - if (token) { - config.headers["Authorization"] = `Bearer ${token}`; - } - const locale = localStorage.getItem("astrbot-locale"); - if (locale) { - config.headers["Accept-Language"] = locale; - } - return config; - }); - - // Keep fetch() calls consistent with axios by automatically attaching the JWT. - // Some parts of the UI use fetch directly; without this, those requests will 401. - // Also handle apiBaseUrl for fetch - const _origFetch = window.fetch.bind(window); - window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { - let url = input; - - // 动态获取当前的 Base URL (可能已被 Store 修改) - const currentBaseUrl = axios.defaults.baseURL; - - // 如果是字符串路径且以 /api 开头,并且配置了 Base URL,则拼接 - if ( - typeof input === "string" && - input.startsWith("/api") && - currentBaseUrl - ) { - // 移除 apiBaseUrl 尾部的斜杠 - const cleanBase = currentBaseUrl.replace(/\/+$/, ""); - // 移除 input 开头的斜杠 - const cleanPath = input.replace(/^\/+/, ""); - url = `${cleanBase}/${cleanPath}`; - } - - const token = localStorage.getItem("token"); - - const headers = new Headers( - init?.headers || - (typeof input !== "string" && "headers" in input - ? (input as Request).headers - : undefined), - ); - if (token && !headers.has("Authorization")) { - headers.set("Authorization", `Bearer ${token}`); - } - - const locale = localStorage.getItem("astrbot-locale"); - if (locale && !headers.has("Accept-Language")) { - headers.set("Accept-Language", locale); +}).catch(error => { + console.error('❌ 新i18n系统初始化失败:', error); + + // 即使i18n初始化失败,也要挂载应用(使用回退机制) + const app = createApp(App); + app.use(router); + const pinia = createPinia(); + app.use(pinia); + app.use(print); + app.use(VueApexCharts); + app.use(vuetify); + app.use(confirmPlugin); + app.mount('#app'); + + // 挂载后同步 Vuetify 主题 + import('./stores/customizer').then(({ useCustomizerStore }) => { + const customizer = useCustomizerStore(pinia); + vuetify.theme.global.name.value = customizer.uiTheme; + const storedPrimary = localStorage.getItem('themePrimary'); + const storedSecondary = localStorage.getItem('themeSecondary'); + if (storedPrimary || storedSecondary) { + const themes = vuetify.theme.themes.value; + ['PurpleTheme', 'PurpleThemeDark'].forEach((name) => { + const theme = themes[name]; + if (!theme?.colors) return; + if (storedPrimary) theme.colors.primary = storedPrimary; + if (storedSecondary) theme.colors.secondary = storedSecondary; + if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary; + if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary; + }); } - - return _origFetch(url, { ...init, headers }); - }; - - loader.config({ - paths: { - vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs", - }, }); +}); - // 初始化新的i18n系统,等待完成后再挂载应用 - setupI18n() - .then(async () => { - console.log("🌍 新i18n系统初始化完成"); - - const app = createApp(App); - app.use(router); - const pinia = createPinia(); - app.use(pinia); - // Initialize API Store with presets - const { useApiStore } = await import("@/stores/api"); - const apiStore = useApiStore(pinia); - apiStore.setPresets(presets); - - app.use(print); - app.use(VueApexCharts); - app.use(vuetify); - app.use(confirmPlugin); - - mountApp(app, pinia); - }) - .catch(async (error) => { - console.error("❌ 新i18n系统初始化失败:", error); - - // 即使i18n初始化失败,也要挂载应用(使用回退机制) - const app = createApp(App); - app.use(router); - const pinia = createPinia(); - app.use(pinia); - - // Initialize API Store with presets - const { useApiStore } = await import("@/stores/api"); - const apiStore = useApiStore(pinia); - apiStore.setPresets(presets); - - app.use(print); - app.use(VueApexCharts); - app.use(vuetify); - app.use(confirmPlugin); - - mountApp(app, pinia); - }); -} - -// 启动应用 -initApp(); +axios.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + } + const locale = localStorage.getItem('astrbot-locale'); + if (locale) { + config.headers['Accept-Language'] = locale; + } + return config; +}); + +// Keep fetch() calls consistent with axios by automatically attaching the JWT. +// Some parts of the UI use fetch directly; without this, those requests will 401. +const _origFetch = window.fetch.bind(window); +window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { + const token = localStorage.getItem('token'); + if (!token) return _origFetch(input, init); + + const headers = new Headers(init?.headers || (typeof input !== 'string' && 'headers' in input ? (input as Request).headers : undefined)); + if (!headers.has('Authorization')) { + headers.set('Authorization', `Bearer ${token}`); + } + const locale = localStorage.getItem('astrbot-locale'); + if (locale && !headers.has('Accept-Language')) { + headers.set('Accept-Language', locale); + } + return _origFetch(input, { ...init, headers }); +}; + +loader.config({ + paths: { + vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs', + }, +}) diff --git a/dashboard/src/stores/api.ts b/dashboard/src/stores/api.ts deleted file mode 100644 index b664c1d95b..0000000000 --- a/dashboard/src/stores/api.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { defineStore } from "pinia"; -import axios from "axios"; - -export type ApiPreset = { - name: string; - url: string; -}; - -export const useApiStore = defineStore({ - id: "api", - state: () => ({ - // 优先从 localStorage 读取用户手动设置的地址 - apiBaseUrl: localStorage.getItem("apiBaseUrl") || "", - configPresets: [] as ApiPreset[], - customPresets: JSON.parse( - localStorage.getItem("customPresets") || "[]", - ) as ApiPreset[], - }), - getters: { - presets: (state): ApiPreset[] => [ - ...state.configPresets, - ...state.customPresets, - ], - }, - actions: { - setPresets(presets: ApiPreset[]) { - this.configPresets = presets; - }, - - addPreset(preset: ApiPreset) { - this.customPresets.push(preset); - localStorage.setItem("customPresets", JSON.stringify(this.customPresets)); - }, - - removePreset(name: string) { - this.customPresets = this.customPresets.filter((p) => p.name !== name); - localStorage.setItem("customPresets", JSON.stringify(this.customPresets)); - }, - - /** - * 设置 API 基础地址 - * @param url 后端地址,例如 http://localhost:6185 - */ - setApiBaseUrl(url: string) { - // 移除尾部斜杠,确保一致性 - const cleanUrl = url ? url.replace(/\/+$/, "") : ""; - - this.apiBaseUrl = cleanUrl; - - if (cleanUrl) { - localStorage.setItem("apiBaseUrl", cleanUrl); - } else { - localStorage.removeItem("apiBaseUrl"); - } - - // 立即更新 axios 配置 - axios.defaults.baseURL = cleanUrl; - }, - - /** - * 初始化 API 配置 - * 通常在应用启动时调用,同步 localStorage 到 axios - */ - init() { - if (this.apiBaseUrl) { - axios.defaults.baseURL = this.apiBaseUrl; - } - }, - }, -}); diff --git a/dashboard/src/views/Settings.vue b/dashboard/src/views/Settings.vue index 027effc343..8ec447dac2 100644 --- a/dashboard/src/views/Settings.vue +++ b/dashboard/src/views/Settings.vue @@ -1,750 +1,488 @@ diff --git a/dashboard/src/views/authentication/auth/LoginPage.vue b/dashboard/src/views/authentication/auth/LoginPage.vue index b659eae275..c647dc8e57 100644 --- a/dashboard/src/views/authentication/auth/LoginPage.vue +++ b/dashboard/src/views/authentication/auth/LoginPage.vue @@ -1,56 +1,23 @@