feat:优化webui多个页面的人机交互,修复插件地址问题,放宽插件id限制,增加高级页面缩进,统计页面快捷按钮,优化新手引导

This commit is contained in:
SengokuCola
2026-05-04 12:46:55 +08:00
parent 75665a4d38
commit 75e9453495
29 changed files with 1101 additions and 831 deletions

View File

@@ -58,7 +58,7 @@ MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute(
LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute()
A_MEMORIX_LEGACY_CONFIG_PATH: Path = (CONFIG_DIR / "a_memorix.toml").resolve().absolute()
MMC_VERSION: str = "1.0.0"
CONFIG_VERSION: str = "8.9.21"
CONFIG_VERSION: str = "8.10.1"
MODEL_CONFIG_VERSION: str = "1.14.6"
logger = get_logger("config")

View File

@@ -63,6 +63,7 @@ class BotConfig(ConfigBase):
json_schema_extra={
"x-widget": "custom",
"x-icon": "tags",
"advanced": True,
},
)
"""别名列表"""
@@ -472,6 +473,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "message-circle-warning",
"advanced": True,
},
)
"""是否启用反馈驱动的延迟记忆纠错任务"""
@@ -482,6 +484,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "clock-4",
"advanced": True,
},
)
"""反馈窗口时长(小时),以 query_memory 执行时间为起点"""
@@ -492,6 +495,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "timer",
"advanced": True,
},
)
"""反馈纠错定时任务轮询间隔(分钟)"""
@@ -503,6 +507,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "list-ordered",
"advanced": True,
},
)
"""反馈纠错每轮最大处理任务数"""
@@ -515,6 +520,7 @@ class MemoryConfig(ConfigBase):
"x-widget": "slider",
"x-icon": "gauge",
"step": 0.01,
"advanced": True,
},
)
"""自动应用纠错动作的最低置信度阈值"""
@@ -526,6 +532,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "messages-square",
"advanced": True,
},
)
"""每个纠错任务最多使用的窗口内用户反馈消息数"""
@@ -535,6 +542,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "filter",
"advanced": True,
},
)
"""是否启用纠错前置预筛(用于减少不必要的模型调用)"""
@@ -544,6 +552,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "sticky-note",
"advanced": True,
},
)
"""是否为受影响 paragraph 写入已纠正旧事实标记"""
@@ -553,6 +562,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "eye-off",
"advanced": True,
},
)
"""是否在用户侧查询中硬过滤带有 stale 标记的 paragraph"""
@@ -562,6 +572,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "user-round-search",
"advanced": True,
},
)
"""是否在反馈纠错后将受影响人物画像加入刷新队列"""
@@ -571,6 +582,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "refresh-ccw",
"advanced": True,
},
)
"""人物画像处于脏队列时,读取是否强制刷新而不直接复用旧快照"""
@@ -580,6 +592,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "clapperboard",
"advanced": True,
},
)
"""是否在反馈纠错后将受影响 source 加入 episode 重建队列"""
@@ -589,6 +602,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "switch",
"x-icon": "ban",
"advanced": True,
},
)
"""episode source 处于重建队列时,是否对用户侧查询做屏蔽"""
@@ -599,6 +613,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "repeat",
"advanced": True,
},
)
"""反馈纠错二阶段一致性后台协调任务轮询间隔(分钟)"""
@@ -610,6 +625,7 @@ class MemoryConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "list-restart",
"advanced": True,
},
)
"""反馈纠错二阶段一致性每轮处理 profile/episode 队列的批大小"""
@@ -1350,6 +1366,7 @@ class ChineseTypoConfig(ConfigBase):
"x-widget": "slider",
"x-icon": "percent",
"step": 0.01,
"advanced": True,
},
)
"""单字替换概率"""
@@ -1359,6 +1376,7 @@ class ChineseTypoConfig(ConfigBase):
json_schema_extra={
"x-widget": "input",
"x-icon": "hash",
"advanced": True,
},
)
"""最小字频阈值"""
@@ -1371,6 +1389,7 @@ class ChineseTypoConfig(ConfigBase):
"x-widget": "slider",
"x-icon": "percent",
"step": 0.1,
"advanced": True,
},
)
"""声调错误概率"""
@@ -1383,6 +1402,7 @@ class ChineseTypoConfig(ConfigBase):
"x-widget": "slider",
"x-icon": "percent",
"step": 0.001,
"advanced": True,
},
)
"""整词替换概率"""

View File

@@ -143,6 +143,33 @@ def _serialize_messages(messages: List[Any]) -> List[Dict[str, Any]]:
return [_serialize_message(message) for message in messages]
def _enrich_session_identity(data: Dict[str, Any]) -> Dict[str, Any]:
"""为监控事件补充会话展示所需的群/用户标识。"""
session_id = data.get("session_id")
if not session_id:
return data
try:
from src.chat.message_receive.chat_manager import chat_manager
chat_stream = chat_manager.get_session_by_session_id(str(session_id))
except Exception:
return data
if chat_stream is None:
return data
session_name = chat_manager.get_session_name(str(session_id))
if session_name:
data.setdefault("session_name", session_name)
data.setdefault("is_group_chat", chat_stream.is_group_session)
data.setdefault("group_id", chat_stream.group_id)
data.setdefault("user_id", chat_stream.user_id)
data.setdefault("platform", chat_stream.platform)
return data
def _serialize_tool_results(tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""标准化最终 planner 卡中的工具结果列表。"""
@@ -266,6 +293,7 @@ async def _broadcast(event: str, data: Dict[str, Any]) -> None:
try:
from src.webui.routers.websocket.manager import websocket_manager
data = _enrich_session_identity(data)
subscription_key = f"{MONITOR_DOMAIN}:{MONITOR_TOPIC}"
total_connections = len(websocket_manager.connections)
subscriber_count = sum(
@@ -291,12 +319,24 @@ async def _broadcast(event: str, data: Dict[str, Any]) -> None:
logger.warning(f"MaiSaka 监控事件广播失败: {exc}", exc_info=True)
async def emit_session_start(session_id: str, session_name: str) -> None:
async def emit_session_start(
session_id: str,
session_name: str,
*,
is_group_chat: bool,
group_id: Optional[str],
user_id: Optional[str],
platform: str,
) -> None:
"""广播会话开始事件。"""
await _broadcast("session.start", {
"session_id": session_id,
"session_name": session_name,
"is_group_chat": is_group_chat,
"group_id": group_id,
"user_id": user_id,
"platform": platform,
"timestamp": time.time(),
})

View File

@@ -46,6 +46,7 @@ from .display.display_utils import build_tool_call_summary_lines, format_token_c
from .display.prompt_cli_renderer import PromptCLIVisualizer
from .display.stage_status_board import remove_stage_status, update_stage_status
from .history_utils import drop_leading_orphan_tool_results
from .monitor_events import emit_session_start
from .reasoning_engine import MaisakaReasoningEngine
from .reply_effect import ReplyEffectTracker
from .reply_effect.image_utils import extract_visual_attachments_from_sequence
@@ -136,6 +137,7 @@ class MaisakaHeartFlowChatting:
self._jargon_miner = JargonMiner(session_id, session_name=session_name)
self._reasoning_engine = MaisakaReasoningEngine(self)
self._monitor_session_start_task: Optional[asyncio.Task[None]] = None
self._tool_registry = ToolRegistry()
self._reply_effect_tracker = ReplyEffectTracker(
session_id=self.session_id,
@@ -144,6 +146,24 @@ class MaisakaHeartFlowChatting:
judge_runner=self._run_reply_effect_judge,
)
self._register_tool_providers()
self._emit_monitor_session_start()
def _emit_monitor_session_start(self) -> None:
"""向 WebUI 监控面板同步当前会话的展示标识。"""
try:
self._monitor_session_start_task = asyncio.create_task(
emit_session_start(
session_id=self.session_id,
session_name=self.session_name,
is_group_chat=self.chat_stream.is_group_session,
group_id=self.chat_stream.group_id,
user_id=self.chat_stream.user_id,
platform=self.chat_stream.platform,
)
)
except RuntimeError:
logger.debug("MaiSaka 监控会话开始事件未发送:当前没有运行中的事件循环")
@staticmethod
def _is_reply_effect_tracking_enabled() -> bool:

View File

@@ -24,7 +24,7 @@ from src.common.logger import get_logger
logger = get_logger("plugin_runtime.runner.manifest_validator")
_SEMVER_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
_PLUGIN_ID_PATTERN = re.compile(r"^[a-z0-9]+(?:[.-][a-z0-9]+)+$")
_PLUGIN_ID_PATTERN = re.compile(r"^[A-Za-z0-9_]+(?:[.-][A-Za-z0-9_]+)+$")
_PACKAGE_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
_HTTP_URL_PATTERN = re.compile(r"^https?://.+$")
@@ -379,7 +379,7 @@ class PluginDependencyDefinition(_StrictManifestModel):
ValueError: 当 ID 不符合规则时抛出。
"""
if not _PLUGIN_ID_PATTERN.fullmatch(value):
raise ValueError("必须使用小写字母/数字,并以点号或横线分隔,例如 github.author.plugin")
raise ValueError("必须使用字母/数字/下划线,并以点号或横线分隔,例如 github.author.plugin")
return value
@field_validator("version_spec")
@@ -548,7 +548,7 @@ class PluginManifest(_StrictManifestModel):
if not value:
raise ValueError("不能为空")
if info.field_name == "id" and not _PLUGIN_ID_PATTERN.fullmatch(value):
raise ValueError("必须使用小写字母/数字,并以点号或横线分隔,例如 github.author.plugin")
raise ValueError("必须使用字母/数字/下划线,并以点号或横线分隔,例如 github.author.plugin")
return value
@field_validator("capabilities")

View File

@@ -185,6 +185,16 @@ def _setup_static_files(app: FastAPI):
logger.warning(t("startup.webui_dashboard_package_hint", command=_MANUAL_INSTALL_COMMAND))
return
@app.get("/maibot_statistics.html", include_in_schema=False)
async def serve_statistics_report():
report_path = (_get_project_root() / "maibot_statistics.html").resolve()
if not report_path.exists() or not report_path.is_file():
raise HTTPException(status_code=404, detail=t("core.not_found"))
response = FileResponse(report_path, media_type="text/html")
response.headers["X-Robots-Tag"] = "noindex, nofollow, noarchive"
return response
@app.get("/{full_path:path}", include_in_schema=False)
async def serve_spa(full_path: str):
if not full_path or full_path == "/":

View File

@@ -19,8 +19,6 @@ class ConfigSchemaGenerator:
for field_name, field_info in config_class.model_fields.items():
if field_name in {"field_docs", "_validate_any", "suppress_any_warning"}:
continue
if cls._is_advanced_field(field_info):
continue
field_schema = cls._build_field_schema(config_class, field_name, field_info.annotation, field_info)
fields.append(field_schema)
@@ -50,13 +48,6 @@ class ConfigSchemaGenerator:
return schema
@staticmethod
def _is_advanced_field(field_info: Any) -> bool:
extra = getattr(field_info, "json_schema_extra", None)
if not isinstance(extra, dict):
return False
return extra.get("advanced", False) is True
@classmethod
def _build_nested_schema(cls, annotation: Any) -> Dict[str, Any] | None:
origin = get_origin(annotation)