Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.12
9 changes: 7 additions & 2 deletions astrbot/cli/commands/cmd_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
else:
click.echo("你可以使用在线面版(v4.14.4+),填写后端地址的方式来控制。")


@click.command()
Expand Down
20 changes: 16 additions & 4 deletions astrbot/cli/commands/cmd_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +18 to +19
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition on line 18 checks if DASHBOARD_ENABLE equals the string "True" (with capital T), but on line 59 in cmd_run.py, it's set to str(not backend_only) which produces "True" or "False". However, the _check_webui_enabled() method in server.py checks for lowercase "true". This inconsistency could cause the dashboard to not be properly disabled when using --backend-only. The check should be case-insensitive or use consistent casing.

Copilot uses AI. Check for mistakes.

log_broker = LogBroker()
LogManager.set_queue_handler(logger, log_broker)
Expand All @@ -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"
Expand All @@ -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("启用插件自动重载")
Expand Down
8 changes: 4 additions & 4 deletions astrbot/cli/utils/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async def check_dashboard(astrbot_root: Path) -> None:
click.echo("正在安装管理面板...")
await download_dashboard(
path="data/dashboard.zip",
extract_path=str(astrbot_root),
extract_path=str(astrbot_root / "data"),
version=f"v{VERSION}",
latest=False,
)
Expand All @@ -54,7 +54,7 @@ async def check_dashboard(astrbot_root: Path) -> None:
click.echo(f"管理面板版本: {version}")
await download_dashboard(
path="data/dashboard.zip",
extract_path=str(astrbot_root),
extract_path=str(astrbot_root / "data"),
version=f"v{VERSION}",
latest=False,
)
Expand All @@ -65,8 +65,8 @@ async def check_dashboard(astrbot_root: Path) -> None:
click.echo("初始化管理面板目录...")
try:
await download_dashboard(
path=str(astrbot_root / "dashboard.zip"),
extract_path=str(astrbot_root),
path=str(astrbot_root / "data" / "dashboard.zip"),
extract_path=str(astrbot_root / "data"),
version=f"v{VERSION}",
latest=False,
)
Expand Down
2 changes: 1 addition & 1 deletion astrbot/core/platform/sources/wecom/wecom_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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,
Expand Down
47 changes: 42 additions & 5 deletions astrbot/core/utils/io.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import base64
import logging
import os
Expand All @@ -7,6 +8,7 @@
import time
import uuid
import zipfile
from ipaddress import IPv4Address, IPv6Address, ip_address
from pathlib import Path

import aiohttp
Expand Down Expand Up @@ -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):
Expand Down
7 changes: 7 additions & 0 deletions astrbot/dashboard/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@
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 .route import Response, 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

Expand Down Expand Up @@ -46,4 +49,8 @@
"ToolsRoute",
"SkillsRoute",
"UpdateRoute",
"T2iRoute",
"LiveChatRoute",
"Response",
"RouteContext",
]
6 changes: 5 additions & 1 deletion astrbot/dashboard/routes/route.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import asdict, dataclass

from quart import Quart

Expand Down Expand Up @@ -57,3 +57,7 @@ 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)
3 changes: 3 additions & 0 deletions astrbot/dashboard/routes/static_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading