pull/1450/head
SengokuCola 2025-12-19 18:18:47 +08:00
commit edfc8d053a
16 changed files with 231 additions and 162 deletions

View File

@ -75,7 +75,6 @@ max_embedding_workers = 12 # 嵌入/抽取并发线程数
embedding_chunk_size = 16 # 每批嵌入的条数
info_extraction_workers = 3 # 实体抽取同时执行线程数
enable_ppr = true # 是否启用PPR低配机器可关闭
ppr_node_cap = 8000 # 图节点数超过该值时自动跳过PPR
```
- `embedding_dimension`
@ -101,8 +100,6 @@ ppr_node_cap = 8000 # 图节点数超过该值时自动跳过PPR
- `true`:检索会结合向量+知识图,效果更好,但略慢;
- `false`:只用向量检索,牺牲一定效果,性能更稳定。
- `ppr_node_cap`
安全阈值:当图节点数超过阈值时自动跳过 PPR以避免“大图”导致卡顿。
> 调参建议:
> - 若导入/检索阶段机器明显“顶不住”(>=1MB的大文本且分配配置<4C

View File

@ -42,7 +42,10 @@ def lpmm_start_up(): # sourcery skip: extract-duplicate-method
logger.info("创建LLM客户端")
# 初始化Embedding库
embed_manager = EmbeddingManager()
embed_manager = EmbeddingManager(
max_workers=global_config.lpmm_knowledge.max_embedding_workers,
chunk_size=global_config.lpmm_knowledge.embedding_chunk_size,
)
logger.info("正在从文件加载Embedding库")
try:
embed_manager.load_from_file()

View File

@ -358,12 +358,9 @@ class KGManager:
paragraph_search_result: ParagraphEmbedding的搜索结果paragraph_hash, similarity
embed_manager: EmbeddingManager对象
"""
# 性能保护:关闭或超限时直接返回向量检索结果(仅基于节点规模与开关)
if (
not global_config.lpmm_knowledge.enable_ppr
or len(self.graph.get_node_list()) > global_config.lpmm_knowledge.ppr_node_cap
):
logger.info("PPR 已禁用或超出阈值,使用纯向量检索结果")
# 性能保护:关闭时直接返回向量检索结果
if not global_config.lpmm_knowledge.enable_ppr:
logger.info("PPR 已禁用,使用纯向量检索结果")
return paragraph_search_result, None
# 图中存在的节点总集
existed_nodes = self.graph.get_node_list()

View File

@ -34,6 +34,7 @@ from src.config.official_configs import (
MemoryConfig,
DebugConfig,
DreamConfig,
WebUIConfig,
)
from .api_ada_configs import (
@ -347,6 +348,7 @@ class Config(ConfigBase):
response_post_process: ResponsePostProcessConfig
response_splitter: ResponseSplitterConfig
telemetry: TelemetryConfig
webui: WebUIConfig
experimental: ExperimentalConfig
maim_message: MaimMessageConfig
lpmm_knowledge: LPMMKnowledgeConfig

View File

@ -597,6 +597,38 @@ class TelemetryConfig(ConfigBase):
"""是否启用遥测"""
@dataclass
class WebUIConfig(ConfigBase):
"""WebUI配置类"""
enabled: bool = True
"""是否启用WebUI"""
mode: Literal["development", "production"] = "production"
"""运行模式development(开发) 或 production(生产)"""
host: str = "0.0.0.0"
"""WebUI服务器监听地址"""
port: int = 8001
"""WebUI服务器端口"""
anti_crawler_mode: Literal["false", "strict", "loose", "basic"] = "basic"
"""防爬虫模式false(禁用) / strict(严格) / loose(宽松) / basic(基础-只记录不阻止)"""
allowed_ips: str = "127.0.0.1"
"""IP白名单逗号分隔支持精确IP、CIDR格式和通配符"""
trusted_proxies: str = ""
"""信任的代理IP列表逗号分隔只有来自这些IP的X-Forwarded-For才被信任"""
trust_xff: bool = False
"""是否启用X-Forwarded-For代理解析默认false"""
secure_cookie: bool = False
"""是否启用安全Cookie仅通过HTTPS传输默认false"""
@dataclass
class DebugConfig(ConfigBase):
"""调试配置类"""
@ -722,6 +754,18 @@ class LPMMKnowledgeConfig(ConfigBase):
embedding_dimension: int = 1024
"""嵌入向量维度,应该与模型的输出维度一致"""
max_embedding_workers: int = 3
"""嵌入/抽取并发线程数"""
embedding_chunk_size: int = 4
"""每批嵌入的条数"""
max_synonym_entities: int = 2000
"""同义边参与的实体数上限,超限则跳过"""
enable_ppr: bool = True
"""是否启用PPR低配机器可关闭"""
@dataclass
class DreamConfig(ConfigBase):

View File

@ -44,10 +44,9 @@ class MainSystem:
def _setup_webui_server(self):
"""设置独立的 WebUI 服务器"""
import os
from src.config.config import global_config
webui_enabled = os.getenv("WEBUI_ENABLED", "false").lower() == "true"
if not webui_enabled:
if not global_config.webui.enabled:
logger.info("WebUI 已禁用")
return

View File

@ -124,10 +124,8 @@ SCANNER_SPECIFIC_HEADERS = {
# strict: 严格模式(更严格的检测,更低的频率限制)
# loose: 宽松模式(较宽松的检测,较高的频率限制)
# basic: 基础模式只记录恶意访问不阻止不限制请求数不跟踪IP
ANTI_CRAWLER_MODE = os.getenv("WEBUI_ANTI_CRAWLER_MODE", "basic").lower()
# IP白名单配置从环境变量读取逗号分隔
# IP白名单配置从配置文件读取逗号分隔
# 支持格式:
# - 精确IP127.0.0.1, 192.168.1.100
# - CIDR格式192.168.1.0/24, 172.17.0.0/16 (适用于Docker网络)
@ -236,13 +234,23 @@ def _convert_wildcard_to_regex(wildcard_pattern: str) -> Optional[str]:
return regex
ALLOWED_IPS = _parse_allowed_ips(os.getenv("WEBUI_ALLOWED_IPS", ""))
# 从配置读取防爬虫设置(延迟导入避免循环依赖)
def _get_anti_crawler_config():
"""获取防爬虫配置"""
from src.config.config import global_config
return {
'mode': global_config.webui.anti_crawler_mode,
'allowed_ips': _parse_allowed_ips(global_config.webui.allowed_ips),
'trusted_proxies': _parse_allowed_ips(global_config.webui.trusted_proxies),
'trust_xff': global_config.webui.trust_xff
}
# 信任的代理IP配置从环境变量读取逗号分隔
# 只有在信任的代理IP下才使用X-Forwarded-For头
# 默认关闭(空),不信任任何代理
TRUSTED_PROXIES = _parse_allowed_ips(os.getenv("WEBUI_TRUSTED_PROXIES", ""))
TRUST_XFF = os.getenv("WEBUI_TRUST_XFF", "false").lower() == "true"
# 初始化配置(将在模块加载时执行)
_config = _get_anti_crawler_config()
ANTI_CRAWLER_MODE = _config['mode']
ALLOWED_IPS = _config['allowed_ips']
TRUSTED_PROXIES = _config['trusted_proxies']
TRUST_XFF = _config['trust_xff']
def _get_mode_config(mode: str) -> dict:

View File

@ -3,10 +3,10 @@ WebUI 认证模块
提供统一的认证依赖支持 Cookie Header 两种方式
"""
import os
from typing import Optional
from fastapi import HTTPException, Cookie, Header, Response, Request
from src.common.logger import get_logger
from src.config.config import global_config
from .token_manager import get_token_manager
logger = get_logger("webui.auth")
@ -23,23 +23,18 @@ def _is_secure_environment() -> bool:
Returns:
bool: 如果应该使用 secure cookie 则返回 True
"""
# 检查环境变量
secure_cookie_env = os.environ.get("WEBUI_SECURE_COOKIE", "")
if secure_cookie_env.lower() in ("true", "1", "yes"):
logger.info(f"WEBUI_SECURE_COOKIE 设置为 {secure_cookie_env},启用 secure cookie")
# 从配置读取
if global_config.webui.secure_cookie:
logger.info("配置中启用了 secure_cookie")
return True
if secure_cookie_env.lower() in ("false", "0", "no"):
logger.info(f"WEBUI_SECURE_COOKIE 设置为 {secure_cookie_env},禁用 secure cookie")
return False
# 检查是否是生产环境
env = os.environ.get("WEBUI_MODE", "").lower()
if env in ("production", "prod"):
logger.info(f"WEBUI_MODE 设置为 {env},启用 secure cookie")
if global_config.webui.mode == "production":
logger.info("WebUI运行在生产模式启用 secure cookie")
return True
# 默认:开发环境不启用(因为通常是 HTTP
logger.debug(f"未设置特殊环境变量 (WEBUI_SECURE_COOKIE={secure_cookie_env}, WEBUI_MODE={env}),禁用 secure cookie")
logger.debug("WebUI运行在开发模式,禁用 secure cookie")
return False
@ -111,7 +106,7 @@ def set_auth_cookie(response: Response, token: str, request: Optional[Request] =
logger.warning("=" * 80)
logger.warning("检测到 HTTP 连接但环境配置要求 HTTPS (secure cookie)")
logger.warning("已自动禁用 secure 标志以允许登录,但建议修改配置:")
logger.warning("1. 在 .env 文件中设置: WEBUI_SECURE_COOKIE=false")
logger.warning("1. 在配置文件中设置: webui.secure_cookie = false")
logger.warning("2. 如果使用反向代理,请确保正确配置 X-Forwarded-Proto 头")
logger.warning("=" * 80)
is_secure = False

View File

@ -1,6 +1,5 @@
"""独立的 WebUI 服务器 - 运行在 0.0.0.0:8001"""
import os
import asyncio
import mimetypes
from pathlib import Path
@ -130,9 +129,10 @@ class WebUIServer:
"""配置防爬虫中间件"""
try:
from src.webui.anti_crawler import AntiCrawlerMiddleware
from src.config.config import global_config
# 从环境变量读取防爬虫模式false/strict/loose/basic
anti_crawler_mode = os.getenv("WEBUI_ANTI_CRAWLER_MODE", "basic").lower()
# 从配置读取防爬虫模式
anti_crawler_mode = global_config.webui.anti_crawler_mode
# 注意:中间件按注册顺序反向执行,所以先注册的中间件后执行
# 我们需要在CORS之前注册这样防爬虫检查会在CORS之前执行
@ -186,7 +186,7 @@ class WebUIServer:
error_msg = f"❌ WebUI 服务器启动失败: 端口 {self.port} 已被占用"
logger.error(error_msg)
logger.error(f"💡 请检查是否有其他程序正在使用端口 {self.port}")
logger.error("💡 可以通过环境变量 WEBUI_PORT 修改 WebUI 端口")
logger.error("💡 可以在配置文件中修改 webui.port 来更改 WebUI 端口")
logger.error(f"💡 Windows 用户可以运行: netstat -ano | findstr :{self.port}")
logger.error(f"💡 Linux/Mac 用户可以运行: lsof -i :{self.port}")
raise OSError(f"端口 {self.port} 已被占用,无法启动 WebUI 服务器")
@ -201,9 +201,21 @@ class WebUIServer:
self._server = UvicornServer(config=config)
logger.info("🌐 WebUI 服务器启动中...")
logger.info(f"🌐 访问地址: http://{self.host}:{self.port}")
if self.host == "0.0.0.0":
logger.info(f"本机访问请使用 http://localhost:{self.port}")
# 根据地址类型显示正确的访问地址
if ':' in self.host:
# IPv6 地址需要用方括号包裹
logger.info(f"🌐 访问地址: http://[{self.host}]:{self.port}")
if self.host == "::":
logger.info(f"💡 IPv6 本机访问: http://[::1]:{self.port}")
logger.info(f"💡 IPv4 本机访问: http://127.0.0.1:{self.port}")
elif self.host == "::1":
logger.info("💡 仅支持 IPv6 本地访问")
else:
# IPv4 地址
logger.info(f"🌐 访问地址: http://{self.host}:{self.port}")
if self.host == "0.0.0.0":
logger.info(f"💡 本机访问: http://localhost:{self.port} 或 http://127.0.0.1:{self.port}")
try:
await self._server.serve()
@ -212,7 +224,7 @@ class WebUIServer:
if "address already in use" in str(e).lower() or e.errno in (98, 10048): # 98: Linux, 10048: Windows
logger.error(f"❌ WebUI 服务器启动失败: 端口 {self.port} 已被占用")
logger.error(f"💡 请检查是否有其他程序正在使用端口 {self.port}")
logger.error("💡 可以通过环境变量 WEBUI_PORT 修改 WebUI 端口")
logger.error("💡 可以在配置文件中修改 webui.port 来更改 WebUI 端口")
else:
logger.error(f"❌ WebUI 服务器启动失败 (网络错误): {e}")
raise
@ -221,14 +233,24 @@ class WebUIServer:
raise
def _check_port_available(self) -> bool:
"""检查端口是否可用"""
"""检查端口是否可用(支持 IPv4 和 IPv6"""
import socket
# 判断使用 IPv4 还是 IPv6
if ':' in self.host:
# IPv6 地址
family = socket.AF_INET6
test_host = self.host if self.host != "::" else "::1"
else:
# IPv4 地址
family = socket.AF_INET
test_host = self.host if self.host != "0.0.0.0" else "127.0.0.1"
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
with socket.socket(family, socket.SOCK_STREAM) as s:
s.settimeout(1)
# 尝试绑定端口
s.bind((self.host if self.host != "0.0.0.0" else "127.0.0.1", self.port))
s.bind((test_host, self.port))
return True
except OSError:
return False
@ -257,8 +279,9 @@ def get_webui_server() -> WebUIServer:
"""获取全局 WebUI 服务器实例"""
global _webui_server
if _webui_server is None:
# 从环境变量读取配置
host = os.getenv("WEBUI_HOST", "0.0.0.0")
port = int(os.getenv("WEBUI_PORT", "8001"))
# 从配置读取
from src.config.config import global_config
host = global_config.webui.host
port = global_config.webui.port
_webui_server = WebUIServer(host=host, port=port)
return _webui_server

View File

@ -196,8 +196,6 @@ max_embedding_workers = 3 # 嵌入/抽取并发线程数
embedding_chunk_size = 4 # 每批嵌入的条数
max_synonym_entities = 2000 # 同义边参与的实体数上限,超限则跳过
enable_ppr = true # 是否启用PPR低配机器可关闭
ppr_node_cap = 8000 # 图节点数超过该值时跳过PPR
webui_graph_default_limit = 200 # WebUI /graph 默认返回的最大节点数,避免大图负载
[keyword_reaction]
keyword_rules = [
@ -265,6 +263,25 @@ api_server_allowed_api_keys = [] # 新版API Server允许的API Key列表
[telemetry] #发送统计信息,主要是看全球有多少只麦麦
enable = true
[webui] # WebUI 独立服务器配置
enabled = true # 是否启用WebUI
mode = "production" # 模式: development(开发) 或 production(生产)
host = "127.0.0.1" # WebUI 服务器监听地址
# IPv4: 0.0.0.0 (所有IPv4接口) / 127.0.0.1 (仅本地)
# IPv6: :: (所有接口支持IPv4+IPv6双栈) / ::1 (仅本地IPv6)
# 推荐使用 :: 实现双栈支持
port = 8001 # WebUI 服务器端口
# 防爬虫配置
anti_crawler_mode = "basic" # 防爬虫模式: false(禁用) / strict(严格) / loose(宽松) / basic(基础-只记录不阻止)
allowed_ips = "127.0.0.1" # IP白名单逗号分隔支持精确IP、CIDR格式和通配符
# 示例: 127.0.0.1,192.168.1.0/24,172.17.0.0/16
trusted_proxies = "" # 信任的代理IP列表逗号分隔只有来自这些IP的X-Forwarded-For才被信任
# 示例: 127.0.0.1,192.168.1.1,172.17.0.1
trust_xff = false # 是否启用X-Forwarded-For代理解析默认false
# 启用后仍要求直连IP在trusted_proxies中才会信任XFF头
secure_cookie = false # 是否启用安全Cookie仅通过HTTPS传输默认false
[experimental] #实验性功能
# 为指定聊天添加额外的prompt配置
# 格式: ["platform:id:type:prompt内容", ...]

View File

@ -1,19 +1,3 @@
# 麦麦主程序配置
HOST=127.0.0.1
PORT=8000
# WebUI 独立服务器配置
WEBUI_ENABLED=true
WEBUI_MODE=production # 模式: development(开发) 或 production(生产)
WEBUI_HOST=0.0.0.0 # WebUI 服务器监听地址
WEBUI_PORT=8001 # WebUI 服务器端口
# 防爬虫配置
WEBUI_ANTI_CRAWLER_MODE=basic # 防爬虫模式: false(禁用) / strict(严格) / loose(宽松) / basic(基础-只记录不阻止)
WEBUI_ALLOWED_IPS=127.0.0.1 # IP白名单逗号分隔支持精确IP、CIDR格式和通配符
# 示例: 127.0.0.1,192.168.1.0/24,172.17.0.0/16
WEBUI_TRUSTED_PROXIES= # 信任的代理IP列表逗号分隔只有来自这些IP的X-Forwarded-For才被信任
# 示例: 127.0.0.1,192.168.1.1,172.17.0.1
WEBUI_TRUST_XFF=false # 是否启用X-Forwarded-For代理解析默认false
# 启用后仍要求直连IP在TRUSTED_PROXIES中才会信任XFF头
WEBUI_SECURE_COOKIE=false # 是否启用安全Cookie仅通过HTTPS传输默认false
PORT=8000

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@
<link rel="icon" type="image/x-icon" href="/maimai.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MaiBot Dashboard</title>
<script type="module" crossorigin src="/assets/index-C8KuzKjI.js"></script>
<script type="module" crossorigin src="/assets/index-BNVFwwkr.js"></script>
<link rel="modulepreload" crossorigin href="/assets/react-vendor-BmxF9s7Q.js">
<link rel="modulepreload" crossorigin href="/assets/router-Bz250laD.js">
<link rel="modulepreload" crossorigin href="/assets/utils-BXc2jIuz.js">
@ -25,7 +25,7 @@
<link rel="modulepreload" crossorigin href="/assets/reactflow-DLoXAt4c.js">
<link rel="modulepreload" crossorigin href="/assets/uppy-CcUbCiwl.js">
<link rel="modulepreload" crossorigin href="/assets/markdown-kUhwkcQP.js">
<link rel="stylesheet" crossorigin href="/assets/index-BwEmxTOV.css">
<link rel="stylesheet" crossorigin href="/assets/index-BNRBabgA.css">
</head>
<body>
<div id="root" class="notranslate"></div>