✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 (#2146)

* ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架

- 【重构】将原 services/llm 重构并迁移至全新的 services/ai 架构,提供向下兼容垫片
- 【新增】引入 Agent、Team、Workflow 三大智能体与工作流编排范式
- 【新增】引入基于 RAG 的长期向量记忆与中期槽位记忆系统
- 【新增】引入基于 Docker 的安全代码执行沙箱环境
- 【新增】支持 MCP 协议,允许动态管理和调用 MCP 服务
- 【新增】引入输入输出安全合规护栏与自愈反思机制
- 【优化】重构并优化多厂商 API 适配器 (Gemini, OpenAI, DeepSeek, GLM 等)
- 【优化】优化日志脱敏与 Token 预估机制
- 【移除】移除旧版 llm default 和 llm reset-key 命令,新增 llm mcp 管理命令

* 🔧 chore(deps): 更新项目依赖与配置

- 添加 mcp、jieba 和 aiodocker 依赖到配置文件及 requirements.txt
- 在 pyright 配置中设置 reportMissingImports 为 none
- 调整 .gitignore 中 resources 目录的忽略规则

* ♻️ refactor(tools): 重构工具终止机制并清理知识库日志输出

- 统一使用 `context.state["__end_run__"]` 替代 `EndRunResult` 控制任务结束
- 移除文件系统和向量知识库检索工具中 `ToolResult` 的 `.with_log` 调用
- 调整指令处理器(Directive)的返回值为 `tool_res.output`
- 修复部分类型检查警告并优化联合类型判断语法

* ♻️ refactor(tools): 重构工具副作用指令与控制流熔断机制

- 引入 `DirectivePayload` 及 `ToolResult` 的子类以结构化表达工具副作用
- 移除通过 `context.state` 传递魔术变量的隐式控制流设计
- 重构 `DirectiveManager` 处理器接口,直接在处理器中修改 `AgentState` 并构建 `AgentRunResult`
- 在 `StandardAgentExecutor` 中统一通过 `directive_manager` 调度工具返回的副作用指令
- 补全 `MessageBuilder` 中部分核心方法的文档注释

* 🐛 fix(sandbox): 修复 Docker 沙箱容器状态检测与会话清理逻辑

-【修复】修正 `is_alive` 中直接读取私有属性的问题,改用 `show()` 返回值
-【修复】解决 `execute_code` 中缓存的执行器与当前会话不一致的问题
-【优化】在清理工作区前增加容器存活检测,避免向已死容器发送请求
-【优化】创建容器时增加运行状态校验,若已停止则自动从缓存中移除并重建
-【优化】优化容器销毁和清理逻辑,静默处理容器不存在 (404) 的异常

* 📝 docs(core): 补充核心模块初始化方法的文档注释

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rumio
2026-07-03 08:53:56 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent bdc1374848
commit 80fc5b86a7
223 changed files with 41885 additions and 10006 deletions
+6 -12
View File
@@ -9,12 +9,9 @@ from zhenxun.models.group_console import GroupConsole
from zhenxun.models.level_user import LevelUser
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.statistics import Statistics
from zhenxun.services import (
LLMException,
LLMMessage,
avatar_service,
generate,
)
from zhenxun.services import avatar_service
from zhenxun.services.ai.core.exceptions import LLMException
from zhenxun.services.ai.llm.api import chat
from zhenxun.services.db_context import with_db_timeout
from zhenxun.services.log import logger
from zhenxun.services.message_load import is_db_unhealthy
@@ -364,12 +361,9 @@ async def get_llm_help(question: str, user_id: str) -> str | bytes:
f"{system_prompt}\n\n=== 功能列表和说明 ===\n{knowledge_base}"
)
messages = [
LLMMessage.system(full_instruction),
LLMMessage.user(question),
]
response = await generate(
messages=messages,
response = await chat(
message=question,
instruction=full_instruction,
model=Config.get_config("help", "DEFAULT_LLM_MODEL"),
)
+116 -43
View File
@@ -1,5 +1,7 @@
from collections import defaultdict
from arclet.alconna import MultiVar
from nonebot.adapters import Event
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import (
@@ -13,6 +15,7 @@ from nonebot_plugin_alconna import (
on_alconna,
store_true,
)
from nonebot_plugin_waiter import prompt
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.log import logger
@@ -35,20 +38,20 @@ __plugin_meta__ = PluginMetadata(
llm info <Provider/ModelName>
- 查看指定模型的详细信息和能力。
llm default [Provider/ModelName]
- 查看或设置全局默认模型。
- 不带参数: 查看当前默认模型。
- 带参数: 设置新的默认模型。
- 例子: llm default Gemini/gemini-2.0-flash
llm test <Provider/ModelName>
- 测试指定模型的连通性和API Key有效性。
llm keys <ProviderName>
- 查看指定提供商的所有API Key状态。
llm reset-key <ProviderName> [--key <api_key>]
- 重置提供商的所有或指定API Key的失败状态。
llm mcp [action] [targets...]
- 管理 MCP (Model Context Protocol) 服务。
- 不带参数: 查看当前配置的 MCP 服务列表及序号。
- 添加/add <JSON>: 动态添加或修改 MCP 配置 (需包含 mcpServers)。
- 开启/关闭 <ID/名称>: 批量切换目标 MCP 的状态。也可以使用 on/off。
- 删除/del <ID/名称>: 删除指定 MCP 服务 (需要确认)。
- 重载/reload: 重新读取 mcp.json 配置文件。
- 例子: llm mcp 开启 1 3 bingcn
""",
extra=PluginExtraData(
author="HibiKier",
@@ -67,18 +70,19 @@ llm_cmd = on_alconna(
help_text="查看模型列表",
),
Subcommand("info", Args["model_name", str], help_text="查看模型详情"),
Subcommand("default", Args["model_name?", str], help_text="查看或设置默认模型"),
Subcommand(
"test", Args["model_name", str], alias=["ping"], help_text="测试模型连通性"
),
Subcommand("keys", Args["provider_name", str], help_text="查看API密钥状态"),
Subcommand(
"reset-key",
Args["provider_name", str],
Option("--key", Args["api_key", str], help_text="指定要重置的API Key"),
help_text="重置API Key状态",
"mcp",
Option("添加", Args["json_strs", MultiVar(str)], alias=["add"]),
Option("开启", Args["targets", MultiVar(str)], alias=["on"]),
Option("关闭", Args["targets", MultiVar(str)], alias=["off"]),
Option("删除", Args["targets", MultiVar(str)], alias=["del"]),
Option("重载", alias=["reload"]),
help_text="管理 MCP 服务",
),
Option("--all", action=store_true, help_text="显示所有条目"),
),
permission=SUPERUSER,
priority=5,
@@ -135,23 +139,6 @@ async def handle_info(arp: Arparma, model_name: Match[str]):
await llm_cmd.finish(MessageUtils.build_message(image_bytes))
@llm_cmd.assign("default")
async def handle_default(arp: Arparma, model_name: Match[str]):
"""处理 'llm default' 命令"""
if model_name.available:
logger.info(
f"设置默认模型为: {model_name.result}",
command="LLM Manage",
session=arp.header_result,
)
_success, message = await DataSource.set_default_model(model_name.result)
await llm_cmd.finish(message)
else:
logger.info("查看默认模型", command="LLM Manage", session=arp.header_result)
current_default = await DataSource.get_default_model()
await llm_cmd.finish(f"当前全局默认模型为: {current_default or '未设置'}")
@llm_cmd.assign("test")
async def handle_test(arp: Arparma, model_name: Match[str]):
"""处理 'llm test' 命令"""
@@ -186,16 +173,102 @@ async def handle_keys(arp: Arparma, provider_name: Match[str]):
await llm_cmd.finish(MessageUtils.build_message(image))
@llm_cmd.assign("reset-key")
async def handle_reset_key(
arp: Arparma, provider_name: Match[str], api_key: Match[str]
):
"""处理 'llm reset-key' 命令"""
key_to_reset = api_key.result if api_key.available else None
log_msg = f"重置 {provider_name.result} 的 " + (
"指定API Key" if key_to_reset else "所有API Keys"
)
logger.info(log_msg, command="LLM Manage", session=arp.header_result)
@llm_cmd.assign("mcp")
async def handle_mcp(arp: Arparma, event: Event):
"""处理 'llm mcp' 命令"""
is_enable = None
targets = ()
_success, message = await DataSource.reset_key(provider_name.result, key_to_reset)
await llm_cmd.finish(message)
if arp.exist("mcp.重载"):
await DataSource.reload_mcp_config()
await llm_cmd.finish("✅ MCP 配置已成功重载并应用!")
if arp.exist("mcp.添加"):
raw_text = event.get_plaintext()
import re
match = re.search(r"\{.*\}", raw_text, re.DOTALL)
if not match:
await llm_cmd.finish("❌ 无法从输入中提取 JSON,请确保包含完整的 {} 括号。")
json_str = match.group(0)
_success, msg = await DataSource.add_mcp_servers_from_json(json_str)
await llm_cmd.finish(msg)
if arp.exist("mcp.删除"):
targets = arp.query("mcp.删除.targets", ())
if isinstance(targets, str):
targets = (targets,)
if not targets:
await llm_cmd.finish(
"请指定需要删除的 MCP ID 或名称,例如:llm mcp del 1 3"
)
valid_names, invalid_targets = await DataSource.resolve_mcp_targets(targets)
if not valid_names:
await llm_cmd.finish(
f"⚠️ 未找到任何有效的 MCP 服务。\n无效目标: {', '.join(invalid_targets)}"
)
confirm_msg = (
f"⚠️ 即将永久删除以下 {len(valid_names)} 个 MCP 服务:\n"
f"{', '.join(valid_names)}\n\n"
"确认删除请在 30 秒内回复「Y」或「是」,取消请回复其他内容。"
)
resp = await prompt(confirm_msg, timeout=30)
if resp is None:
await llm_cmd.finish("⏳ 等待超时,已自动取消删除操作。")
user_input = resp.extract_plain_text().strip().lower()
if user_input not in {"y", "yes", "是", "1", "确认", "ok"}:
await llm_cmd.finish("🛑 已取消删除操作。")
await DataSource.delete_mcp_servers(valid_names)
await llm_cmd.finish(f"🗑️ 已成功删除 MCP 服务: {', '.join(valid_names)}")
if arp.exist("mcp.开启"):
is_enable = True
targets = arp.query("mcp.开启.targets", ())
elif arp.exist("mcp.关闭"):
is_enable = False
targets = arp.query("mcp.关闭.targets", ())
if is_enable is None:
logger.info("获取 MCP 列表", command="LLM Manage", session=arp.header_result)
mcp_list = await DataSource.get_mcp_list()
image = await Presenters.format_mcp_list_as_image(mcp_list)
await llm_cmd.finish(MessageUtils.build_message(image))
if not targets:
await llm_cmd.finish(
"请指定需要操作的 MCP ID 或名称,例如:llm mcp 开启 1 3 bingcn"
)
if isinstance(targets, str):
targets = (targets,)
logger.info(
f"批量{'开启' if is_enable else '关闭'} MCP: {targets}",
command="LLM Manage",
session=arp.header_result,
)
success_names, invalid_targets = await DataSource.toggle_mcp_servers(
targets, is_enable
)
msg_parts = []
if success_names:
status_txt = "开启" if is_enable else "关闭"
msg_parts.append(
f"✅ 已成功{status_txt} {len(success_names)} 个"
f"MCP 服务:\n{', '.join(success_names)}"
)
if invalid_targets:
msg_parts.append(f"⚠️ 以下 ID 或名称无效被忽略:\n{', '.join(invalid_targets)}")
if not msg_parts:
msg_parts.append("没有任何配置被修改。")
await llm_cmd.finish("\n\n".join(msg_parts))
@@ -1,18 +1,15 @@
import json
import time
from typing import Any
from zhenxun.services.llm import (
LLMException,
get_global_default_model_name,
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.ai.core.exceptions import LLMException
from zhenxun.services.ai.llm.api import chat
from zhenxun.services.ai.llm.manager import (
get_model_instance,
list_available_models,
set_global_default_model_name,
)
from zhenxun.services.llm.core import KeyStatus
from zhenxun.services.llm.manager import (
reset_key_status,
)
from zhenxun.services.llm.types import LLMMessage
from zhenxun.services.ai.tools.providers.mcp.provider import mcp_provider
class DataSource:
@@ -39,27 +36,12 @@ class DataSource:
except LLMException:
return None
@staticmethod
async def get_default_model() -> str | None:
"""获取全局默认模型"""
return get_global_default_model_name()
@staticmethod
async def set_default_model(model_name_str: str) -> tuple[bool, str]:
"""设置全局默认模型"""
success = set_global_default_model_name(model_name_str)
if success:
return True, f"✅ 成功将默认模型设置为: {model_name_str}"
else:
return False, f"❌ 设置失败,模型 '{model_name_str}' 不存在或无效。"
@staticmethod
async def test_model_connectivity(model_name_str: str) -> tuple[bool, str]:
"""测试模型连通性"""
start_time = time.monotonic()
try:
async with await get_model_instance(model_name_str) as model:
await model.generate_response([LLMMessage.user("你好")])
await chat("你好", model=model_name_str)
end_time = time.monotonic()
latency = (end_time - start_time) * 1000
return (
@@ -70,7 +52,7 @@ class DataSource:
return (
False,
f"❌ 模型 '{model_name_str}' 连接测试失败:\n"
f"{e.user_friendly_message}\n错误码: {e.code.name}",
f"{e.user_friendly_message}\n错误类型: {e.__class__.__name__}",
)
except Exception as e:
return False, f"❌ 测试时发生未知错误: {e!s}"
@@ -78,7 +60,7 @@ class DataSource:
@staticmethod
async def get_key_status(provider_name: str) -> list[dict[str, Any]] | None:
"""获取并排序指定提供商的API Key状态"""
from zhenxun.services.llm.manager import get_key_usage_stats
from zhenxun.services.ai.llm.manager import get_key_usage_stats
all_stats = await get_key_usage_stats()
provider_stats = all_stats.get(provider_name)
@@ -93,11 +75,30 @@ class DataSource:
]
def sort_key(item: dict[str, Any]):
status_priority = item.get("status_enum", KeyStatus.UNUSED).value
status_map = {
"DISABLED": 0,
"ERROR": 1,
"COOLDOWN": 2,
"WARNING": 3,
"HEALTHY": 4,
"UNUSED": 5,
}
status_str = item.get("status", "HEALTHY")
if (
item.get("successes", 0) == 0
and item.get("failures", 0) == 0
and status_str == "HEALTHY"
):
status_str = "UNUSED"
status_priority = status_map.get(status_str, 5)
total = item.get("successes", 0) + item.get("failures", 0)
success_rate = (
(item.get("successes", 0) / total * 100) if total > 0 else 100.0
)
return (
status_priority,
100 - item.get("success_rate", 100.0),
-item.get("total_calls", 0),
100 - success_rate,
-total,
)
sorted_stats_list = sorted(stats_list, key=sort_key)
@@ -105,17 +106,159 @@ class DataSource:
return sorted_stats_list
@staticmethod
async def reset_key(provider_name: str, api_key: str | None) -> tuple[bool, str]:
"""重置API Key状态"""
success = await reset_key_status(provider_name, api_key)
if success:
if api_key:
if len(api_key) > 8:
target = f"API Key '{api_key[:4]}...{api_key[-4:]}'"
else:
target = f"API Key '{api_key}'"
async def get_mcp_list() -> list[dict[str, Any]]:
"""获取排序后的 MCP 列表"""
await mcp_provider.initialize()
if not mcp_provider._config:
return []
mcp_servers = mcp_provider._config.mcpServers
sorted_names = sorted(mcp_servers.keys())
result = []
for idx, name in enumerate(sorted_names):
conf = mcp_servers[name]
target = ""
if conf.transport in ("stdio", "sandbox_proxy") and conf.command:
target = f"{conf.command} {' '.join(conf.args)}"
elif conf.transport in ("sse", "streamable-http") and conf.url:
target = conf.url
result.append(
{
"id": idx + 1,
"name": name,
"enabled": conf.enabled,
"transport": conf.transport,
"target": target,
}
)
return result
@staticmethod
async def resolve_mcp_targets(
targets: tuple[Any, ...],
) -> tuple[list[str], list[str]]:
"""将输入的 ID 或名称解析为实际的 MCP 服务名称"""
await mcp_provider.initialize()
if not mcp_provider._config:
return [], list(map(str, targets))
mcp_servers = mcp_provider._config.mcpServers
sorted_names = sorted(mcp_servers.keys())
valid_names = []
invalid_targets = []
for tgt in targets:
tgt_str = str(tgt)
target_name = None
if tgt_str.isdigit():
idx = int(tgt_str) - 1
if 0 <= idx < len(sorted_names):
target_name = sorted_names[idx]
else:
target = "所有API Keys"
return True, f"✅ 成功重置提供商 '{provider_name}' 的 {target} 的状态。"
else:
return False, "❌ 重置失败,请检查提供商名称或API Key是否正确。"
if tgt_str in mcp_servers:
target_name = tgt_str
if target_name:
valid_names.append(target_name)
else:
invalid_targets.append(tgt_str)
return list(dict.fromkeys(valid_names)), list(dict.fromkeys(invalid_targets))
@staticmethod
async def toggle_mcp_servers(
targets: tuple[Any, ...], is_enable: bool
) -> tuple[list[str], list[str]]:
"""批量切换 MCP 状态"""
valid_names, invalid_targets = await DataSource.resolve_mcp_targets(targets)
if not mcp_provider._config:
return [], invalid_targets
mcp_servers = mcp_provider._config.mcpServers
success_names = []
for target_name in valid_names:
conf = mcp_servers[target_name]
if conf.enabled != is_enable:
conf.enabled = is_enable
if not is_enable:
if tk := mcp_provider._toolkits.pop(target_name, None):
await tk.close()
else:
if target_name not in mcp_provider._toolkits:
mcp_provider._setup_toolkit(target_name, conf)
success_names.append(target_name)
if success_names:
mcp_provider._discovered_tools = None
mcp_provider._save_config()
return success_names, invalid_targets
@staticmethod
async def reload_mcp_config() -> None:
"""完全重新加载 MCP 配置"""
await mcp_provider.shutdown()
mcp_provider._config = None
mcp_provider._discovered_tools = None
await mcp_provider.initialize()
@staticmethod
async def delete_mcp_servers(names: list[str]) -> None:
"""删除指定的 MCP 服务"""
for name in names:
await mcp_provider.unregister_server(name)
@staticmethod
async def add_mcp_servers_from_json(json_str: str) -> tuple[bool, str]:
"""将 JSON 字符串解析并合并到 mcp.json"""
mcp_path = DATA_PATH / "ai" / "mcp.json"
try:
json_str = json_str.strip()
if json_str.startswith("```"):
lines = json_str.split("\n")
if lines[0].startswith("```"):
lines = lines[1:]
if lines and lines[-1].startswith("```"):
lines = lines[:-1]
json_str = "\n".join(lines).strip()
new_config = json.loads(json_str)
if not isinstance(new_config, dict) or "mcpServers" not in new_config:
return False, "❌ JSON 格式不正确,必须包含顶层键 'mcpServers'。"
new_servers = new_config["mcpServers"]
if not isinstance(new_servers, dict) or not new_servers:
return False, "❌ 'mcpServers' 不能为空且必须为 JSON 对象(dict)。"
if mcp_path.exists():
with mcp_path.open("r", encoding="utf-8") as f:
current_config = json.load(f)
else:
current_config = {"mcpServers": {}}
if "mcpServers" not in current_config:
current_config["mcpServers"] = {}
added_names = []
for name, conf in new_servers.items():
current_config["mcpServers"][name] = conf
added_names.append(name)
mcp_path.parent.mkdir(parents=True, exist_ok=True)
with mcp_path.open("w", encoding="utf-8") as f:
json.dump(current_config, f, ensure_ascii=False, indent=2)
await DataSource.reload_mcp_config()
return True, f"✅ 成功添加/更新 MCP 服务: {', '.join(added_names)}"
except json.JSONDecodeError as e:
return False, f"❌ JSON 解析失败: {e}"
except Exception as e:
return False, f"❌ 添加 MCP 服务时发生未知错误: {e}"
@@ -1,9 +1,9 @@
from typing import Any
import time
from typing import Any, Literal
from zhenxun import ui
from zhenxun.services import renderer_service
from zhenxun.services.llm.core import KeyStatus
from zhenxun.services.llm.types import ModelModality
from zhenxun.services.ai.core.models import ModelModality
from zhenxun.ui.models import StatusBadgeCell, TextCell
@@ -72,7 +72,7 @@ class Presenters:
cap_list = []
if ModelModality.IMAGE in caps.input_modalities:
cap_list.append("视觉")
cap_list.append("图片")
if ModelModality.VIDEO in caps.input_modalities:
cap_list.append("视频")
if ModelModality.AUDIO in caps.input_modalities:
@@ -93,11 +93,16 @@ class Presenters:
md.head("模型详情", 2)
temp_value = model.temperature or provider.temperature or "未设置"
token_value = model.max_tokens or provider.max_tokens or "未设置"
input_tokens = caps.max_input_tokens
context_window = (
f"{int(input_tokens / 1000)}K"
if input_tokens >= 1000
else str(input_tokens)
)
md.text(f"- **名称**: {model.model_name}")
md.text(f"- **默认温度**: {temp_value}")
md.text(f"- **最大Token**: {token_value}")
md.text(f"- **上下文窗口**: {context_window}")
md.text(f"- **核心能力**: {', '.join(cap_list) or '纯文本'}")
return await renderer_service.render(md.with_style("light"))
@@ -112,33 +117,41 @@ class Presenters:
data_list = []
for key_info in sorted_stats:
status_enum: KeyStatus = key_info["status_enum"]
status_str = key_info.get("status", "HEALTHY")
successes = key_info.get("successes", 0)
failures = key_info.get("failures", 0)
total_calls = successes + failures
if status_enum == KeyStatus.COOLDOWN:
cooldown_seconds = int(key_info["cooldown_seconds_left"])
if total_calls == 0 and status_str == "HEALTHY":
status_str = "UNUSED"
if status_str == "COOLDOWN":
cooldown_seconds = max(
0, int(key_info.get("cooldown_until", 0) - time.time())
)
formatted_time = _format_seconds(cooldown_seconds)
status_cell = StatusBadgeCell(
text=f"冷却中({formatted_time})", status_type="info"
)
else:
status_map = {
KeyStatus.DISABLED: ("永久禁用", "error"),
KeyStatus.ERROR: ("错误", "error"),
KeyStatus.WARNING: ("告警", "warning"),
KeyStatus.HEALTHY: ("健康", "ok"),
KeyStatus.UNUSED: ("未使用", "info"),
status_map: dict[
str,
tuple[str, Literal["ok", "error", "warning", "info", "success"]],
] = {
"DISABLED": ("永久禁用", "error"),
"ERROR": ("错误", "error"),
"WARNING": ("告警", "warning"),
"HEALTHY": ("健康", "ok"),
"UNUSED": ("未使用", "info"),
}
text, status_type = status_map.get(status_enum, ("未知", "info"))
status_cell = StatusBadgeCell(text=text, status_type=status_type) # type: ignore
text, status_type = status_map.get(status_str, ("未知", "info"))
status_cell = StatusBadgeCell(text=text, status_type=status_type)
total_calls = key_info["total_calls"]
total_calls_text = (
f"{key_info['success_count']}/{total_calls}"
if total_calls > 0
else "0/0"
f"{successes}/{total_calls}" if total_calls > 0 else "0/0"
)
success_rate = key_info["success_rate"]
success_rate = (successes / total_calls * 100) if total_calls > 0 else 100.0
success_rate_text = f"{success_rate:.1f}%" if total_calls > 0 else "N/A"
rate_color = None
if total_calls > 0:
@@ -148,13 +161,18 @@ class Presenters:
rate_color = "#E6A23C"
success_rate_cell = TextCell(content=success_rate_text, color=rate_color)
avg_latency = key_info["avg_latency"]
avg_latency_text = f"{avg_latency / 1000:.2f}" if avg_latency > 0 else "N/A"
avg_latency_text = "N/A"
last_error = key_info.get("last_error") or "-"
if len(last_error) > 25:
last_error = last_error[:22] + "..."
suggested_action = "-"
if status_str == "DISABLED":
suggested_action = "检查配额或换Key"
elif status_str == "COOLDOWN":
suggested_action = "等待恢复"
data_list.append(
[
TextCell(content=key_info["key_id"]),
@@ -163,7 +181,7 @@ class Presenters:
success_rate_cell,
TextCell(content=avg_latency_text),
TextCell(content=last_error),
TextCell(content=key_info["suggested_action"]),
TextCell(content=suggested_action),
]
)
@@ -181,3 +199,38 @@ class Presenters:
)
table.add_rows(data_list)
return await renderer_service.render(table, use_cache=False)
@staticmethod
async def format_mcp_list_as_image(mcp_list: list[dict[str, Any]]) -> bytes:
"""将MCP列表格式化为表格图片"""
title = "MCP 服务管理列表"
if not mcp_list:
table = ui.table(title=title, tip="当前未配置任何 MCP 服务。").set_headers(
["ID", "MCP名称", "协议", "状态", "目标"]
)
return await renderer_service.render(table)
column_name = ["ID", "MCP名称", "协议", "状态", "目标"]
rows_data = []
for mcp in mcp_list:
is_enable = mcp["enabled"]
status_type = "success" if is_enable else "info"
status_text = "开启" if is_enable else "关闭"
rows_data.append(
[
TextCell(content=str(mcp["id"])),
TextCell(content=mcp["name"]),
TextCell(content=mcp["transport"]),
StatusBadgeCell(text=status_text, status_type=status_type),
TextCell(content=mcp["target"]),
]
)
table = ui.table(
title=title,
tip="使用 `llm mcp 开启/关闭 <ID/名称>` 来修改状态,支持批量操作",
)
table.set_headers(column_name)
table.set_column_alignments(["center", "left", "left", "center", "left"])
table.add_rows(rows_data)
return await renderer_service.render(table, use_cache=False)
@@ -9,8 +9,8 @@ from nonebot_plugin_session import EventSession
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.llm.config.providers import get_llm_config
from zhenxun.services.llm.manager import clear_model_cache
from zhenxun.services.ai.config import get_llm_config
from zhenxun.services.ai.llm.manager import clear_all_cache
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
@@ -108,7 +108,7 @@ async def _reload_plugin_limit_config() -> None:
async def _reload_runtime_config() -> None:
Config.reload()
get_llm_config.cache_clear()
clear_model_cache()
clear_all_cache()
await _reload_plugin_limit_config()
with contextlib.suppress(Exception):
_reschedule_auto_reload_job()