mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-07 04:40:00 +08:00
✨ 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:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bdc1374848
commit
80fc5b86a7
@@ -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"),
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user