f88031b0c9
* Refactor: using sqlmodel(sqlchemy+pydantic) as ORM framework and switch to async-based sqlite operation (#2294) * stage * stage * refactor: using sqlchemy as ORM framework, switch to async-based sqlite operation - using sqlmodel as ORM(based on sqlchemy and pydantic) - add Persona, Preference, PlatformMessageHistory table * fix: conversation * fix: remove redundant explicit session.commit, and fix some type error * fix: conversation context issue * chore: remove comments * chore: remove exclude_content param * Fix: 当多个相同消息平台实例部署时上下文可能混乱(共享) (#2298) * perf: update astrbot event session format, using platfrom id to ensure uniqueness fixes: #1000 * fix: 更新 MessageSession 类以使用 platform_id 作为唯一标识符,并调整相关方法以确保一致性 * fix: 更新 MessageSession 文档以明确 platform_id 的赋值规则,并调整 get_platform 和 get_platform_inst 方法的返回类型 * Improve: 引入全新的人格管理模式以及重构函数工具管理器 (#2305) * feat: add persona management * refactor: 重构函数工具管理器,引入 ToolSet,并让 Persona 支持绑定 Tools * feat: 更新 Persona 工具选择逻辑,支持全选和指定工具的切换 * feat: 更新 BaseDatabase 中的 persona 方法返回类型,支持返回 None * fix: platform id * feat: add support to sync mcp servers from ModelScope (#2313) * fix: 修复访问令牌的空格问题 * chore: 移除 MCP 市场相关逻辑 (#2314) * chore: 移除 MCP 市场相关路由 * Refactor: 重构配置文件管理,以支持更灵活的、会话粒度的(基于 umo part)配置文件隔离 (#2328) * refactor: 重构配置文件管理,以支持更灵活的、基于 umo part 的配置文件隔离 * Refactor: 重构配置前端页面,新增数个配置项 (#2331) * refactor: 重构配置前端页面,新增数个配置项 * feat: 完善多配置文件结构 * perf: 系统配置入口 * fix: normal config item list not display * fix: 修复 axios 请求中的上下文引用问题 * chore: remove status checking in chat page * fix: 修复 stage 在不同 pipeline 中被重复使用的问题和 persona 相关问题 * Feature: 增加图片转述提供商配置、支持用户自定义模型模态能力 (#2422) * feat: 增加图片转述提供商配置、支持用户自定义模型模态能力 * fix: 修复 LLMRequestSubStage 中会话管理方法参数不一致的问题,简化方法调用 * Feature: 优化 WebSearch 的爬取网页速度并且支持使用 Tavily 作为搜索引擎 (#2427) * feat: 优化了 websearch 的速度;支持 Tavily 作为搜索引擎 * fix: 优化日志记录格式,修复搜索结果处理中的索引和内容显示问题 * feat: 添加对话选中状态管理,优化默认对话加载逻辑 * feat: 支持通过解析URL 的方式导入网页数据到知识库 (#2280) * feat:为webchat页面添加一个手动上传文件按钮(目前只处理图片) * fix:上传后清空value,允许触发change事件以多次上传同一张图片 * perf:webchat页面消息发送后清空图片预览缩略图,维持与文本信息行为一致 * perf:将文件输入的值重置为空字符串以提升浏览器兼容性 * feat:webchat文件上传按钮支持多选文件上传 * fix:释放blob URL以防止内存泄漏 * perf:并行化sendMessage中的图片获取逻辑 * feat:完成从url获取部分的UI * feat: 添加从URL导入功能的组件 * fix: 优化导入结果处理,添加整体摘要和主题摘要的文件命名 * perf: 更新url导入选项添加默认值 * perf: 在导入url的部分配置项未启用时隐藏暂不使用的下拉框选项 * feat: 添加上传前提提示信息至导入url至知识库功能 * feat: 更新导入功能提示信息,添加上传状态通知 * fix: 优化url转知识库错误处理 * feat: 合并知识库的上传文件和 URL 标签页 * feat: 删除导入URL至知识库功能的相关组件 --------- Co-authored-by: Soulter <905617992@qq.com> * feat: 添加条件显示逻辑以优化插件配置项的可见性管理 (#2433) * Feature: 支持在 WebUI 配置文件页中配置默认知识库 (#2437) * feat: 支持配置默认知识库 * chore: clean code * refactor: 重构 Function Tool 管理并初步引入 Multi Agent 及 Agent Handsoff 机制 (#2454) * stage * refactor: 重构 Function Tool 管理并引入 multi agent handsoff 机制 - Updated `star_request.py` to use the global `call_handler` instead of context-specific calls. - Modified `entities.py` to remove the dependency on `FunctionToolManager` and streamline the function tool handling. - Refactored `func_tool_manager.py` to simplify the `FunctionTool` class and its methods, removing deprecated code and enhancing clarity. - Adjusted `provider.py` to align with the new function tool structure, removing unnecessary type unions. - Enhanced `star_handler.py` to support agent registration and tool association, introducing `RegisteringAgent` for better encapsulation. - Updated `star_manager.py` to handle tool registration for agents, ensuring proper binding of handlers. - Revised `main.py` in the web searcher package to utilize the new agent registration system for web search tools. * chore: websearch * perf: 减少嵌套 * chore: 移除未使用的 mcp 导入 * feat: 添加 WebUI 迁移助手以及相关迁移方法 (#2477) * fix: 修复迁移对话时的一些问题 * feat: 增加工具使用模型能力选项 * feat: 添加知识库插件更新检查和更新功能 * perf: 调整 WebUI sidebar 顺序 * refactor: 重构 SharedPreference 类并采用数据库存储替换 json 存储 (#2482) * perf: 使用 run_coroutine_threadsafe Co-authored-by: Raven95676 <raven95676@gmail.com> * Feature: 支持配置重排序模型(vLLM API 格式)用于 score 任务 (#2496) * feat: 支持添加重排序模型(vLLM API 格式)用于 score 任务 * fix: update rerank API base URL to use localhost * feat: 知识库支持配置重排序模型 * fix: remove debug print statement for reranked results in FaissVecDB * fix: 移除知识库中的提示文本 * Feature: 支持在配置文件配置可用的插件组 (#2505) * feat: 增加可用插件集合配置项 * remove: 旧版平台可用性配置 已经基于多配置文件实现。 * feat: 应用配置文件插件可用性配置 * perf: hoist if from if * feat: llm_tool 装饰器返回值支持返回 mcp 库中 tool 的返回值类型(mcp.type.CallToolResult) (#2507) * fix: add type definition for migrationDialog and ensure open method exists before calling * chore: update project version to 4.0.0 * feat: 多 t2i 服务的随机负载均衡 (#2529) * fix: bugfixes * Improve: 扩大配置文件生效范围的自定义程度到会话粒度 (#2532) * feat: 扩大配置文件生效范围的自定义程度 * perf: 冲突检测 * refactor: simplify config form validation and improve conflict message clarity * chore: clean code * feat: 插件配置支持多个快捷魔法配置项 * chore: 修复当自动更新 webchat title 时,history 被重置的问题 * bugfixes * feat: add custom T2I template editor (#2581) * perf: add option to clear provider selection in ProviderSelector component * 📦 release: bump verstion to v4.0.0-beta.1 * chore: delete uv.lock --------- Co-authored-by: RC-CHN <67079377+RC-CHN@users.noreply.github.com> Co-authored-by: Raven95676 <raven95676@gmail.com>
220 lines
8.5 KiB
Python
220 lines
8.5 KiB
Python
import asyncio
|
||
import logging
|
||
import os
|
||
import socket
|
||
|
||
import jwt
|
||
import psutil
|
||
from quart import Quart, g, jsonify, request
|
||
from quart.logging import default_handler
|
||
|
||
from astrbot.core import logger
|
||
from astrbot.core.config.default import VERSION
|
||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||
from astrbot.core.db import BaseDatabase
|
||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||
from astrbot.core.utils.io import get_local_ip_addresses
|
||
|
||
from .routes import *
|
||
from .routes.route import Response, RouteContext
|
||
from .routes.session_management import SessionManagementRoute
|
||
|
||
APP: Quart = None
|
||
|
||
|
||
class AstrBotDashboard:
|
||
def __init__(
|
||
self,
|
||
core_lifecycle: AstrBotCoreLifecycle,
|
||
db: BaseDatabase,
|
||
shutdown_event: asyncio.Event,
|
||
) -> None:
|
||
self.core_lifecycle = core_lifecycle
|
||
self.config = core_lifecycle.astrbot_config
|
||
self.data_path = os.path.abspath(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
|
||
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
|
||
)
|
||
self.sr = StatRoute(self.context, db, core_lifecycle)
|
||
self.pr = PluginRoute(
|
||
self.context, core_lifecycle, core_lifecycle.plugin_manager
|
||
)
|
||
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.chat_route = ChatRoute(self.context, db, core_lifecycle)
|
||
self.tools_root = ToolsRoute(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, core_lifecycle
|
||
)
|
||
self.persona_route = PersonaRoute(
|
||
self.context, db, core_lifecycle
|
||
)
|
||
|
||
self.app.add_url_rule(
|
||
"/api/plug/<path:subpath>",
|
||
view_func=self.srv_plug_route,
|
||
methods=["GET", "POST"],
|
||
)
|
||
|
||
self.shutdown_event = shutdown_event
|
||
|
||
self._init_jwt_secret()
|
||
|
||
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):
|
||
if not request.path.startswith("/api"):
|
||
return
|
||
allowed_endpoints = ["/api/auth/login", "/api/file"]
|
||
if any(request.path.startswith(prefix) for prefix in allowed_endpoints):
|
||
return
|
||
# claim jwt
|
||
token = request.headers.get("Authorization")
|
||
if not token:
|
||
r = jsonify(Response().error("未授权").__dict__)
|
||
r.status_code = 401
|
||
return r
|
||
if token.startswith("Bearer "):
|
||
token = token[7:]
|
||
try:
|
||
payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"])
|
||
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
|
||
|
||
def check_port_in_use(self, 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} 时发生错误: {str(e)}")
|
||
# 如果出现异常,保守起见认为端口可能被占用
|
||
return True
|
||
|
||
def get_process_using_port(self, port: int) -> str:
|
||
"""获取占用端口的进程详细信息"""
|
||
try:
|
||
for conn in psutil.net_connections(kind="inet"):
|
||
if 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"无法获取进程详细信息(可能需要管理员权限): {str(e)}"
|
||
return "未找到占用进程"
|
||
except Exception as e:
|
||
return f"获取进程信息失败: {str(e)}"
|
||
|
||
def _init_jwt_secret(self):
|
||
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 = []
|
||
if p := os.environ.get("DASHBOARD_PORT"):
|
||
port = p
|
||
else:
|
||
port = self.core_lifecycle.astrbot_config["dashboard"].get("port", 6185)
|
||
host = self.core_lifecycle.astrbot_config["dashboard"].get("host", "0.0.0.0")
|
||
|
||
logger.info(f"正在启动 WebUI, 监听地址: http://{host}:{port}")
|
||
|
||
if host == "0.0.0.0":
|
||
logger.info(
|
||
"提示: WebUI 将监听所有网络接口,请注意安全。(可在 data/cmd_config.json 中配置 dashboard.host 以修改 host)"
|
||
)
|
||
|
||
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. 如需使用其他端口,请修改配置文件"
|
||
)
|
||
|
||
raise Exception(f"端口 {port} 已被占用")
|
||
|
||
display = f"\n ✨✨✨\n AstrBot v{VERSION} WebUI 已启动,可访问\n\n"
|
||
display += f" ➜ 本地: http://localhost:{port}\n"
|
||
for ip in ip_addr:
|
||
display += f" ➜ 网络: http://{ip}:{port}\n"
|
||
display += " ➜ 默认用户名和密码: astrbot\n ✨✨✨\n"
|
||
|
||
if not ip_addr:
|
||
display += (
|
||
"可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n"
|
||
)
|
||
|
||
logger.info(display)
|
||
|
||
return self.app.run_task(
|
||
host=host, port=port, shutdown_trigger=self.shutdown_trigger
|
||
)
|
||
|
||
async def shutdown_trigger(self):
|
||
await self.shutdown_event.wait()
|
||
logger.info("AstrBot WebUI 已经被优雅地关闭")
|