恢复分支

This commit is contained in:
LIghtJUNction
2026-02-27 22:20:38 +08:00
parent f79f460b89
commit fed11fffa4
27 changed files with 2116 additions and 1221 deletions
+1 -1
View File
@@ -1 +1 @@
3.12
3.12
+7 -2
View File
@@ -34,8 +34,13 @@ 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}")
await check_dashboard(astrbot_root / "data")
if click.confirm(
"是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)",
default=True,
):
await check_dashboard(astrbot_root / "data")
else:
click.echo("你可以使用在线面版(v4.14.4+),填写后端地址的方式来控制。")
@click.command()
+16 -4
View File
@@ -15,7 +15,8 @@ async def run_astrbot(astrbot_root: Path) -> None:
from astrbot.core import LogBroker, LogManager, db_helper, logger
from astrbot.core.initial_loader import InitialLoader
await check_dashboard(astrbot_root / "data")
if os.environ.get("DASHBOARD_ENABLE") == "True":
await check_dashboard(astrbot_root / "data")
log_broker = LogBroker()
LogManager.set_queue_handler(logger, log_broker)
@@ -27,9 +28,17 @@ async def run_astrbot(astrbot_root: Path) -> None:
@click.option("--reload", "-r", is_flag=True, help="插件自动重载")
@click.option("--port", "-p", help="Astrbot Dashboard端口", required=False, type=str)
@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.command()
def run(reload: bool, port: str) -> None:
def run(reload: bool, host: str, port: str, backend_only: bool) -> None:
"""运行 AstrBot"""
try:
os.environ["ASTRBOT_CLI"] = "1"
@@ -43,8 +52,11 @@ def run(reload: bool, port: str) -> None:
os.environ["ASTRBOT_ROOT"] = str(astrbot_root)
sys.path.insert(0, str(astrbot_root))
if port:
if port is not None:
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("启用插件自动重载")
+1 -1
View File
@@ -195,7 +195,7 @@ DEFAULT_CONFIG = {
"username": "astrbot",
"password": "77b90590a8945a7d36c963981a307dc9",
"jwt_secret": "",
"host": "0.0.0.0",
"host": "::",
"port": 6185,
"disable_access_log": True,
"ssl": {
@@ -419,7 +419,7 @@ class AiocqhttpAdapter(Platform):
def run(self) -> Awaitable[Any]:
if not self.host or not self.port:
logger.warning(
"aiocqhttp: 未配置 ws_reverse_host 或 ws_reverse_port,将使用默认值:http://0.0.0.0:6199",
"aiocqhttp: 未配置 ws_reverse_host 或 ws_reverse_port,将使用默认值:http://[::]:6199",
)
self.host = "0.0.0.0"
self.port = 6199
@@ -21,7 +21,7 @@ class QQOfficialWebhook:
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", "0.0.0.0")
self.callback_server_host = config.get("callback_server_host", "::")
if isinstance(self.port, str):
self.port = int(self.port)
@@ -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", "0.0.0.0")
self.callback_server_host = config.get("callback_server_host", "::")
self.server.add_url_rule(
"/callback/command",
view_func=self.verify,
@@ -407,7 +407,7 @@ class WecomPlatformAdapter(Platform):
abm.message = [Image(file=path, url=path)]
elif msgtype == "voice":
media_id = msg.get("voice", {}).get("media_id", "")
resp: Response = await asyncio.get_event_loop().run_in_executor(
resp = await asyncio.get_event_loop().run_in_executor(
None,
self.client.media.download,
media_id,
+42 -5
View File
@@ -1,3 +1,4 @@
import asyncio
import base64
import logging
import os
@@ -7,6 +8,7 @@ import ssl
import time
import uuid
import zipfile
from ipaddress import IPv4Address, IPv6Address, ip_address
from pathlib import Path
import aiohttp
@@ -206,18 +208,53 @@ def file_to_base64(file_path: str) -> str:
return "base64://" + base64_str
def get_local_ip_addresses():
def get_local_ip_addresses() -> list[IPv4Address | IPv6Address]:
net_interfaces = psutil.net_if_addrs()
network_ips = []
network_ips: list[IPv4Address | IPv6Address] = []
for interface, addrs in net_interfaces.items():
for _, addrs in net_interfaces.items():
for addr in addrs:
if addr.family == socket.AF_INET: # 使用 socket.AF_INET 代替 psutil.AF_INET
network_ips.append(addr.address)
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)
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):
+8
View File
@@ -9,16 +9,20 @@ from .conversation import ConversationRoute
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
@@ -46,4 +50,8 @@ __all__ = [
"ToolsRoute",
"SkillsRoute",
"UpdateRoute",
"T2iRoute",
"LiveChatRoute",
"Response",
"RouteContext",
]
+5 -1
View File
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import asdict, dataclass
from quart import Quart
@@ -57,3 +57,7 @@ class Response:
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)
+3
View File
@@ -5,6 +5,9 @@ class StaticFileRoute(Route):
def __init__(self, context: RouteContext) -> None:
super().__init__(context)
if "index" in self.app.view_functions:
return
index_ = [
"/",
"/auth/login",
+266 -178
View File
@@ -2,9 +2,12 @@ import asyncio
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, cast
from typing import Protocol
import jwt
import psutil
@@ -13,6 +16,7 @@ from hypercorn.asyncio import serve
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
@@ -23,13 +27,6 @@ from astrbot.core.utils.io import get_local_ip_addresses
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):
@@ -46,6 +43,16 @@ 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,
@@ -56,67 +63,123 @@ class AstrBotDashboard:
self.core_lifecycle = core_lifecycle
self.config = core_lifecycle.astrbot_config
self.db = db
self.shutdown_event = shutdown_event
# 参数指定webui目录
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):
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")
)
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)
self.context = RouteContext(self.config, self.app)
self.ur = UpdateRoute(
self.context,
core_lifecycle.astrbot_updator,
core_lifecycle,
def _init_app(self):
"""初始化 Quart 应用"""
global APP
self.app = Quart(
"AstrBotDashboard",
static_folder=self.data_path,
static_url_path="/",
)
self.sr = StatRoute(self.context, db, core_lifecycle)
self.pr = PluginRoute(
self.context,
core_lifecycle,
core_lifecycle.plugin_manager,
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.before_request(self.auth_middleware)
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
)
StatRoute(self.context, db, self.core_lifecycle)
PluginRoute(
self.context, self.core_lifecycle, self.core_lifecycle.plugin_manager
)
self.command_route = CommandRoute(self.context)
self.cr = ConfigRoute(self.context, core_lifecycle)
self.lr = LogRoute(self.context, core_lifecycle.log_broker)
self.cr = ConfigRoute(self.context, self.core_lifecycle)
self.lr = LogRoute(self.context, self.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, core_lifecycle)
self.chat_route = ChatRoute(self.context, db, self.core_lifecycle)
self.open_api_route = OpenApiRoute(
self.context,
db,
core_lifecycle,
self.core_lifecycle,
self.chat_route,
)
self.chatui_project_route = ChatUIProjectRoute(self.context, db)
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.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.file_route = FileRoute(self.context)
self.session_management_route = SessionManagementRoute(
self.context,
db,
core_lifecycle,
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.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.app.add_url_rule(
"/api/plug/<path:subpath>",
@@ -124,20 +187,35 @@ class AstrBotDashboard:
methods=["GET", "POST"],
)
self.shutdown_event = shutdown_event
def _init_plugin_route_index(self):
"""将插件路由索引,避免 O(n) 查找"""
self._plugin_route_map: dict[tuple[str, str], Callable] = {}
self._init_jwt_secret()
for (
route,
handler,
methods,
_,
) in self.core_lifecycle.star_context.registered_web_apis:
for method in methods:
self._plugin_route_map[(route, method)] = handler
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__)
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"]
# ------------------------------------------------------------------
# Middleware中间件
# ------------------------------------------------------------------
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"):
@@ -174,33 +252,46 @@ class AstrBotDashboard:
await self.db.touch_api_key(api_key.key_id)
return None
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):
if any(request.path.startswith(p) for p in self.ALLOWED_ENDPOINT_PREFIXES):
return None
# 声明 JWT
token = request.headers.get("Authorization")
if not token:
r = jsonify(Response().error("未授权").__dict__)
r.status_code = 401
return r
token = token.removeprefix("Bearer ")
return self._unauthorized("未授权")
try:
payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"])
payload = jwt.decode(
token.removeprefix("Bearer "),
self._jwt_secret,
algorithms=["HS256"],
options={"require": ["username"]},
)
g.username = payload["username"]
except jwt.ExpiredSignatureError:
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
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())
@staticmethod
def _extract_raw_api_key() -> str | None:
@@ -230,126 +321,87 @@ class AstrBotDashboard:
}
return scope_map.get(path)
def check_port_in_use(self, port: int) -> bool:
def check_port_in_use(self, host: str, port: int) -> bool:
"""跨平台检测端口是否被占用"""
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
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind((host, port))
return False
except OSError:
return True
def get_process_using_port(self, port: int) -> str:
"""获取占用端口的进程详细信息"""
"""获取占用端口的进程信息"""
try:
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 "未找到占用进程"
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
except Exception as e:
return f"获取进程信息失败: {e!s}"
return "未知进程"
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)
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"
)
host = (
os.environ.get("DASHBOARD_HOST")
or os.environ.get("ASTRBOT_DASHBOARD_HOST")
or dashboard_config.get("host", "0.0.0.0")
port = int(
os.environ.get("DASHBOARD_PORT") or dashboard_config.get("port", 6185)
)
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")
or os.environ.get("ASTRBOT_DASHBOARD_SSL_ENABLE"),
bool(ssl_config.get("enable", False)),
os.environ.get("DASHBOARD_SSL_ENABLE"),
ssl_config.get("enable", False),
)
scheme = "https" if ssl_enable else "http"
display_host = f"[{host}]" if ":" in host else host
if not enable:
logger.info("WebUI 已被禁用")
return None
logger.info(f"正在启动 WebUI, 监听地址: {scheme}://{host}:{port}")
if host == "0.0.0.0":
if self.enable_webui:
logger.info(
"提示: WebUI 将监听所有网络接口,请注意安全。(可在 data/cmd_config.json 中配置 dashboard.host 以修改 host",
"正在启动 WebUI + API, 监听地址: %s://%s:%s",
scheme,
display_host,
port,
)
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)
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}")
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. 如需使用其他端口,请修改配置文件",
)
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"
)
logger.info(display)
if self.enable_webui:
self._print_access_urls(host, port, scheme)
# 配置 Hypercorn
config = HyperConfig()
config.bind = [f"{host}:{port}"]
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
if ssl_enable:
cert_file = (
os.environ.get("DASHBOARD_SSL_CERT")
@@ -392,12 +444,48 @@ class AstrBotDashboard:
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 serve(self.app, config, shutdown_trigger=self.shutdown_trigger)
return asyncio.run(
serve(self.app, config, shutdown_trigger=self.shutdown_trigger)
)
async def shutdown_trigger(self) -> None:
@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))
async def shutdown_trigger(self):
await self.shutdown_event.wait()
logger.info("AstrBot WebUI 已经被优雅地关闭")
+6
View File
@@ -7,3 +7,9 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
+13
View File
@@ -0,0 +1,13 @@
{
"apiBaseUrl": "",
"presets": [
{
"name": "Default (Auto)",
"url": ""
},
{
"name": "Localhost",
"url": "http://localhost:6185"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -10,5 +10,16 @@
"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"
}
}
}
@@ -1,6 +1,14 @@
{
"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.",
@@ -10,5 +10,16 @@
"theme": {
"switchToDark": "切换到深色主题",
"switchToLight": "切换到浅色主题"
},
"serverConfig": {
"title": "服务器配置",
"description": "如果后端服务不在同源(主机/端口不同),请在此指定完整 URL。",
"label": "API 基础地址",
"placeholder": "例如:http://localhost:6185",
"hint": "留空以使用默认设置(相对路径)",
"presetLabel": "快速选择预设",
"save": "保存并刷新",
"cancel": "取消",
"tooltip": "服务器配置"
}
}
}
@@ -1,6 +1,14 @@
{
"network": {
"title": "网络",
"server": {
"title": "服务器地址",
"subtitle": "配置后端 API 地址",
"label": "API 基础地址",
"placeholder": "例如:http://localhost:6185",
"hint": "留空以使用默认设置(相对路径)",
"save": "保存并刷新"
},
"githubProxy": {
"title": "GitHub 加速地址",
"subtitle": "设置下载插件或者更新 AstrBot 时所用的 GitHub 加速地址。这在中国大陆的网络环境有效。可以自定义,输入结果实时生效。所有地址均不保证稳定性,如果在更新插件/项目时出现报错,请首先检查加速地址是否能正常使用。",
+160 -95
View File
@@ -1,116 +1,181 @@
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 { 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';
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");
// 初始化新的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;
});
}
});
}).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;
});
}
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;
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;
});
// 动态获取当前的 Base URL (可能已被 Store 修改)
const currentBaseUrl = axios.defaults.baseURL;
// 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);
// 如果是字符串路径且以 /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 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 });
};
const token = localStorage.getItem("token");
loader.config({
paths: {
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs',
},
})
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);
}
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();
+70
View File
@@ -0,0 +1,70 @@
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;
}
},
},
});
File diff suppressed because it is too large Load Diff
@@ -1,23 +1,56 @@
<script setup lang="ts">
import AuthLogin from '../authForms/AuthLogin.vue';
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
import { onMounted, ref } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { useRouter } from 'vue-router';
import AuthLogin from "../authForms/AuthLogin.vue";
import LanguageSwitcher from "@/components/shared/LanguageSwitcher.vue";
import { onMounted, ref } from "vue";
import { useAuthStore } from "@/stores/auth";
import { useApiStore } from "@/stores/api";
import { useRouter } from "vue-router";
import { useCustomizerStore } from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
import { useTheme } from 'vuetify';
import { useModuleI18n } from "@/i18n/composables";
import { useTheme } from "vuetify";
const cardVisible = ref(false);
const router = useRouter();
const authStore = useAuthStore();
const apiStore = useApiStore();
const customizer = useCustomizerStore();
const { tm: t } = useModuleI18n('features/auth');
const { tm: t } = useModuleI18n("features/auth");
const theme = useTheme();
const serverConfigDialog = ref(false);
const apiUrl = ref(apiStore.apiBaseUrl);
const showAddPreset = ref(false);
const newPresetName = ref("");
const newPresetUrl = ref("");
function saveApiUrl() {
apiStore.setApiBaseUrl(apiUrl.value);
serverConfigDialog.value = false;
window.location.reload();
}
function savePreset() {
if (!newPresetName.value || !newPresetUrl.value) return;
apiStore.addPreset({
name: newPresetName.value,
url: newPresetUrl.value,
});
showAddPreset.value = false;
newPresetName.value = "";
newPresetUrl.value = "";
}
function isCustomPreset(name: string) {
return apiStore.customPresets.some((p) => p.name === name);
}
// 主题切换函数
function toggleTheme() {
const newTheme = customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark';
const newTheme =
customizer.uiTheme === "PurpleThemeDark"
? "PurpleTheme"
: "PurpleThemeDark";
customizer.SET_UI_THEME(newTheme);
theme.global.name.value = newTheme;
}
@@ -25,7 +58,7 @@ function toggleTheme() {
onMounted(() => {
// 检查用户是否已登录,如果已登录则重定向
if (authStore.has_token()) {
router.push(authStore.returnUrl || '/');
router.push(authStore.returnUrl || "/");
return;
}
@@ -41,28 +74,178 @@ onMounted(() => {
<v-card class="login-card" elevation="1">
<v-card-title>
<div class="d-flex justify-space-between align-center w-100">
<img width="80" src="@/assets/images/icon-no-shadow.svg" alt="AstrBot Logo">
<img
width="80"
src="@/assets/images/icon-no-shadow.svg"
alt="AstrBot Logo"
/>
<div class="d-flex align-center gap-1">
<LanguageSwitcher />
<v-divider vertical class="mx-1"
style="height: 24px !important; opacity: 0.9 !important; align-self: center !important; border-color: rgba(180, 148, 246, 0.8) !important;"></v-divider>
<v-btn @click="toggleTheme" class="theme-toggle-btn" icon variant="text" size="small">
<v-icon size="18" :color="useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'">
<v-divider
vertical
class="mx-1"
style="
height: 24px !important;
opacity: 0.9 !important;
align-self: center !important;
border-color: rgba(180, 148, 246, 0.8) !important;
"
></v-divider>
<v-btn
@click="serverConfigDialog = true"
icon
variant="text"
size="small"
>
<v-icon
size="18"
:color="
useCustomizerStore().uiTheme === 'PurpleTheme'
? '#5e35b1'
: '#d7c5fa'
"
>
mdi-server
</v-icon>
<v-tooltip activator="parent" location="top">
{{ t("serverConfig.tooltip") }}
</v-tooltip>
</v-btn>
<v-btn
@click="toggleTheme"
class="theme-toggle-btn"
icon
variant="text"
size="small"
>
<v-icon
size="18"
:color="
useCustomizerStore().uiTheme === 'PurpleTheme'
? '#5e35b1'
: '#d7c5fa'
"
>
mdi-white-balance-sunny
</v-icon>
<v-tooltip activator="parent" location="top">
{{ t('theme.switchToLight') }}
{{ t("theme.switchToLight") }}
</v-tooltip>
</v-btn>
</div>
</div>
<div class="ml-2" style="font-size: 26px;">{{ t('logo.title') }}</div>
<div class="mt-2 ml-2" style="font-size: 14px; color: grey;">{{ t('logo.subtitle') }}</div>
<div class="ml-2" style="font-size: 26px">{{ t("logo.title") }}</div>
<div class="mt-2 ml-2" style="font-size: 14px; color: grey">
{{ t("logo.subtitle") }}
</div>
</v-card-title>
<v-card-text>
<AuthLogin />
</v-card-text>
</v-card>
<v-dialog v-model="serverConfigDialog" max-width="450">
<v-card>
<v-card-title>{{ t("serverConfig.title") }}</v-card-title>
<v-card-text>
<div class="text-body-2 text-medium-emphasis mb-4">
{{ t("serverConfig.description") }}
</div>
<div
v-if="
(apiStore.presets && apiStore.presets.length > 0) ||
apiStore.customPresets
"
class="mb-4"
>
<div class="d-flex justify-space-between align-center mb-2">
<div class="text-caption text-medium-emphasis">
{{ t("serverConfig.presetLabel") }}
</div>
<v-btn
size="x-small"
variant="text"
icon
@click="showAddPreset = !showAddPreset"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
<v-expand-transition>
<div
v-if="showAddPreset"
class="mb-2 pa-2 bg-grey-lighten-4 rounded border"
>
<v-text-field
v-model="newPresetName"
label="Name"
density="compact"
hide-details
class="mb-2"
variant="outlined"
bg-color="white"
></v-text-field>
<v-text-field
v-model="newPresetUrl"
label="URL"
density="compact"
hide-details
class="mb-2"
variant="outlined"
bg-color="white"
></v-text-field>
<v-btn
size="small"
block
color="primary"
variant="flat"
@click="savePreset"
>Add Preset</v-btn
>
</div>
</v-expand-transition>
<v-chip-group column>
<v-chip
v-for="preset in apiStore.presets"
:key="preset.name"
size="small"
@click="apiUrl = preset.url"
:variant="apiUrl === preset.url ? 'flat' : 'tonal'"
:color="apiUrl === preset.url ? 'primary' : undefined"
:closable="isCustomPreset(preset.name)"
@click:close="apiStore.removePreset(preset.name)"
>
{{ preset.name }}
</v-chip>
</v-chip-group>
</div>
<v-text-field
v-model="apiUrl"
:label="t('serverConfig.label')"
:placeholder="t('serverConfig.placeholder')"
:hint="t('serverConfig.hint')"
persistent-hint
variant="outlined"
density="compact"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="text" @click="serverConfigDialog = false">{{
t("serverConfig.cancel")
}}</v-btn>
<v-btn color="primary" variant="flat" @click="saveApiUrl">{{
t("serverConfig.save")
}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
+21 -5
View File
@@ -1,15 +1,31 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/types/.d.ts"],
"compilerOptions": {
"ignoreDeprecations": "5.0",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"allowJs": true
"allowJs": true,
"ignoreDeprecations": "5.0"
},
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue",
"src/types/.d.ts"
],
"references": [
{
"path": "./tsconfig.vite-config.json"
+5 -3
View File
@@ -1,9 +1,11 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["vite.config.*"],
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"allowJs": true,
"types": ["node"]
}
},
"include": ["vite.config.*"]
}
+24 -24
View File
@@ -1,7 +1,7 @@
import { fileURLToPath, URL } from 'url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vuetify from 'vite-plugin-vuetify';
import { fileURLToPath, URL } from "url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vuetify from "vite-plugin-vuetify";
// https://vitejs.dev/config/
export default defineConfig({
@@ -9,42 +9,42 @@ export default defineConfig({
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => ['v-list-recognize-title'].includes(tag)
}
}
isCustomElement: (tag) => ["v-list-recognize-title"].includes(tag),
},
},
}),
vuetify({
autoImport: true
})
autoImport: true,
}),
],
resolve: {
alias: {
mermaid: 'mermaid/dist/mermaid.js',
'@': fileURLToPath(new URL('./src', import.meta.url))
}
mermaid: "mermaid/dist/mermaid.js",
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
css: {
preprocessorOptions: {
scss: {}
}
scss: {},
},
},
build: {
sourcemap: false,
chunkSizeWarningLimit: 1024 * 1024 // Set the limit to 1 MB
chunkSizeWarningLimit: 1024 * 1024, // Set the limit to 1 MB
},
optimizeDeps: {
exclude: ['vuetify'],
entries: ['./src/**/*.vue']
exclude: ["vuetify"],
entries: ["./src/**/*.vue"],
},
server: {
host: '0.0.0.0',
host: "::",
port: 3000,
proxy: {
'/api': {
target: 'http://127.0.0.1:6185/',
"/api": {
target: "http://127.0.0.1:6185/",
changeOrigin: true,
ws: true
}
}
}
ws: true,
},
},
},
});
+2 -1
View File
@@ -63,6 +63,7 @@ dependencies = [
"shipyard-python-sdk>=0.2.4",
"python-socks>=2.8.0",
"packaging>=24.2",
"quart-cors>=0.8.0",
]
[dependency-groups]
@@ -92,7 +93,7 @@ select = [
"Q", # flake8-quotes
"I", # import-order
"UP", # pyupgrade
# "SIM", # flake8-simplify
# "SIM", # flake8-simplify
]
ignore = [
"F403",