fix(mcp): split categories by server and preserve toggle states
This commit is contained in:
parent
2c75bb01a0
commit
3c1773dc01
@ -63,7 +63,7 @@ from modules.skills_manager import (
|
||||
)
|
||||
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
|
||||
from modules.custom_tool_executor import CustomToolExecutor
|
||||
from modules.mcp_server_registry import build_default_mcp_category
|
||||
from modules.mcp_server_registry import MCPServerRegistry, build_default_mcp_category
|
||||
|
||||
try:
|
||||
from config.limits import THINKING_FAST_INTERVAL
|
||||
@ -196,22 +196,37 @@ class MainTerminalToolsDefinitionMixin:
|
||||
def _build_mcp_tools(self) -> List[Dict]:
|
||||
if not getattr(self, "mcp_tools_enabled", False):
|
||||
if hasattr(self, "tool_categories_map") and "mcp" in self.tool_categories_map:
|
||||
self.tool_categories_map["mcp"].tools = []
|
||||
self.tool_categories_map["mcp"].tools = ["list_mcp_servers"]
|
||||
stale_keys = [
|
||||
key for key in list(getattr(self, "tool_categories_map", {}).keys())
|
||||
if isinstance(key, str) and key.startswith("mcp_server__")
|
||||
]
|
||||
for key in stale_keys:
|
||||
self.tool_categories_map.pop(key, None)
|
||||
self.tool_category_states.pop(key, None)
|
||||
self._refresh_disabled_tools()
|
||||
self.mcp_tool_alias_map = {}
|
||||
return []
|
||||
if self._is_mcp_disabled_in_docker_mode():
|
||||
self.mcp_tool_alias_map = {}
|
||||
mcp_category_tools = ["list_mcp_servers"]
|
||||
if "mcp" in self.tool_categories_map:
|
||||
self.tool_categories_map["mcp"].tools = mcp_category_tools
|
||||
self.tool_categories_map["mcp"].tools = ["list_mcp_servers"]
|
||||
elif getattr(self, "mcp_tools_enabled", False):
|
||||
default_mcp_cat = build_default_mcp_category()
|
||||
self.tool_categories_map["mcp"] = type(next(iter(TOOL_CATEGORIES.values())))(
|
||||
label=default_mcp_cat["label"],
|
||||
tools=mcp_category_tools,
|
||||
tools=["list_mcp_servers"],
|
||||
default_enabled=True,
|
||||
silent_when_disabled=False,
|
||||
)
|
||||
stale_keys = [
|
||||
key for key in list(getattr(self, "tool_categories_map", {}).keys())
|
||||
if isinstance(key, str) and key.startswith("mcp_server__")
|
||||
]
|
||||
for key in stale_keys:
|
||||
self.tool_categories_map.pop(key, None)
|
||||
self.tool_category_states.pop(key, None)
|
||||
self._refresh_disabled_tools()
|
||||
return []
|
||||
# 类别层面的禁用优先生效(即使策略里尚未填充具体工具列表)
|
||||
mcp_cat = getattr(self, "tool_categories_map", {}).get("mcp")
|
||||
@ -222,7 +237,15 @@ class MainTerminalToolsDefinitionMixin:
|
||||
mcp_enabled = forced
|
||||
if not mcp_enabled:
|
||||
self.mcp_tool_alias_map = {}
|
||||
self.tool_categories_map["mcp"].tools = []
|
||||
self.tool_categories_map["mcp"].tools = ["list_mcp_servers"]
|
||||
stale_keys = [
|
||||
key for key in list(getattr(self, "tool_categories_map", {}).keys())
|
||||
if isinstance(key, str) and key.startswith("mcp_server__")
|
||||
]
|
||||
for key in stale_keys:
|
||||
self.tool_categories_map.pop(key, None)
|
||||
self.tool_category_states.pop(key, None)
|
||||
self._refresh_disabled_tools()
|
||||
return []
|
||||
manager = getattr(self, "mcp_client_manager", None)
|
||||
if manager is None:
|
||||
@ -238,22 +261,97 @@ class MainTerminalToolsDefinitionMixin:
|
||||
tool_defs, alias_map = [], {}
|
||||
|
||||
# 保存映射,供执行层按 alias 分发到具体 server/tool
|
||||
self.mcp_tool_alias_map = alias_map or {}
|
||||
mcp_category_tools = ["list_mcp_servers", *list((alias_map or {}).keys())]
|
||||
|
||||
# 同步 mcp 分类列表,避免策略页显示旧数据
|
||||
alias_map = alias_map or {}
|
||||
if "mcp" in self.tool_categories_map:
|
||||
self.tool_categories_map["mcp"].tools = mcp_category_tools
|
||||
self.tool_categories_map["mcp"].tools = ["list_mcp_servers"]
|
||||
elif getattr(self, "mcp_tools_enabled", False):
|
||||
default_mcp_cat = build_default_mcp_category()
|
||||
self.tool_categories_map["mcp"] = type(next(iter(TOOL_CATEGORIES.values())))(
|
||||
label=default_mcp_cat["label"],
|
||||
tools=mcp_category_tools,
|
||||
tools=["list_mcp_servers"],
|
||||
default_enabled=True,
|
||||
silent_when_disabled=False,
|
||||
)
|
||||
server_aliases: Dict[str, List[str]] = {}
|
||||
for alias, binding in alias_map.items():
|
||||
sid = str(getattr(binding, "server_id", "") or "").strip()
|
||||
if not sid:
|
||||
continue
|
||||
server_aliases.setdefault(sid, []).append(alias)
|
||||
|
||||
return tool_defs
|
||||
enabled_servers = []
|
||||
try:
|
||||
enabled_servers = list(self.mcp_server_registry.list_enabled_servers())
|
||||
except Exception:
|
||||
enabled_servers = []
|
||||
|
||||
active_server_category_ids = set()
|
||||
for server in enabled_servers:
|
||||
sid = str(server.get("id") or "").strip()
|
||||
if not sid:
|
||||
continue
|
||||
cat_id = MCPServerRegistry.make_server_category_id(sid)
|
||||
active_server_category_ids.add(cat_id)
|
||||
aliases = list(server_aliases.get(sid, []))
|
||||
existing = self.tool_categories_map.get(cat_id)
|
||||
label = MCPServerRegistry.make_server_category_label(server)
|
||||
if existing is not None:
|
||||
existing.label = label
|
||||
existing.tools = aliases
|
||||
else:
|
||||
self.tool_categories_map[cat_id] = type(next(iter(TOOL_CATEGORIES.values())))(
|
||||
label=label,
|
||||
tools=aliases,
|
||||
default_enabled=True,
|
||||
silent_when_disabled=False,
|
||||
)
|
||||
if cat_id not in self.tool_category_states:
|
||||
self.tool_category_states[cat_id] = True
|
||||
|
||||
stale_keys = [
|
||||
key for key in list(self.tool_categories_map.keys())
|
||||
if isinstance(key, str)
|
||||
and key.startswith("mcp_server__")
|
||||
and key not in active_server_category_ids
|
||||
]
|
||||
for key in stale_keys:
|
||||
self.tool_categories_map.pop(key, None)
|
||||
self.tool_category_states.pop(key, None)
|
||||
|
||||
if hasattr(self, "_prune_runtime_tool_overrides"):
|
||||
self._prune_runtime_tool_overrides()
|
||||
if hasattr(self, "_apply_runtime_tool_overrides"):
|
||||
self._apply_runtime_tool_overrides()
|
||||
|
||||
filtered_tools: List[Dict[str, Any]] = []
|
||||
filtered_alias_map: Dict[str, Any] = {}
|
||||
for tool_def in tool_defs:
|
||||
alias = str(((tool_def or {}).get("function") or {}).get("name") or "").strip()
|
||||
if not alias:
|
||||
continue
|
||||
binding = alias_map.get(alias)
|
||||
if binding is None:
|
||||
continue
|
||||
sid = str(getattr(binding, "server_id", "") or "").strip()
|
||||
if not sid:
|
||||
continue
|
||||
cat_id = MCPServerRegistry.make_server_category_id(sid)
|
||||
cat = self.tool_categories_map.get(cat_id)
|
||||
cat_enabled = self.tool_category_states.get(
|
||||
cat_id,
|
||||
getattr(cat, "default_enabled", True) if cat is not None else True,
|
||||
)
|
||||
forced = getattr(self, "admin_forced_category_states", {}).get(cat_id)
|
||||
if isinstance(forced, bool):
|
||||
cat_enabled = forced
|
||||
if not cat_enabled:
|
||||
continue
|
||||
filtered_tools.append(tool_def)
|
||||
filtered_alias_map[alias] = binding
|
||||
|
||||
self.mcp_tool_alias_map = filtered_alias_map
|
||||
self._refresh_disabled_tools()
|
||||
return filtered_tools
|
||||
|
||||
def define_tools(self) -> List[Dict]:
|
||||
"""定义可用工具(添加确认工具)"""
|
||||
|
||||
@ -87,6 +87,34 @@ DISABLE_LENGTH_CHECK = True
|
||||
PERMISSION_MODES = {"readonly", "approval", "unrestricted"}
|
||||
|
||||
class MainTerminalToolsPolicyMixin:
|
||||
def _ensure_runtime_tool_overrides(self) -> Dict[str, bool]:
|
||||
overrides = getattr(self, "runtime_tool_category_overrides", None)
|
||||
if not isinstance(overrides, dict):
|
||||
overrides = {}
|
||||
self.runtime_tool_category_overrides = overrides
|
||||
return overrides
|
||||
|
||||
def _prune_runtime_tool_overrides(self) -> None:
|
||||
overrides = self._ensure_runtime_tool_overrides()
|
||||
valid_keys = set(getattr(self, "tool_categories_map", {}).keys())
|
||||
for key in list(overrides.keys()):
|
||||
if key not in valid_keys:
|
||||
overrides.pop(key, None)
|
||||
|
||||
def _apply_runtime_tool_overrides(self) -> None:
|
||||
overrides = self._ensure_runtime_tool_overrides()
|
||||
if not overrides:
|
||||
return
|
||||
categories = getattr(self, "tool_categories_map", {}) or {}
|
||||
for key, value in list(overrides.items()):
|
||||
if key not in categories:
|
||||
overrides.pop(key, None)
|
||||
continue
|
||||
forced = getattr(self, "admin_forced_category_states", {}).get(key)
|
||||
if isinstance(forced, bool):
|
||||
continue
|
||||
self.tool_category_states[key] = bool(value)
|
||||
|
||||
def apply_personalization_preferences(self, config: Optional[Dict[str, Any]] = None):
|
||||
"""Apply persisted personalization settings that affect runtime behavior."""
|
||||
try:
|
||||
@ -141,6 +169,8 @@ class MainTerminalToolsPolicyMixin:
|
||||
# Reset category states to defaults before applying overrides
|
||||
for key, category in self.tool_categories_map.items():
|
||||
self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled
|
||||
self._prune_runtime_tool_overrides()
|
||||
self._apply_runtime_tool_overrides()
|
||||
self._refresh_disabled_tools()
|
||||
|
||||
# 默认模型偏好(优先应用,再处理运行模式)
|
||||
@ -205,7 +235,9 @@ class MainTerminalToolsPolicyMixin:
|
||||
forced = self.admin_forced_category_states.get(category)
|
||||
if isinstance(forced, bool) and forced != enabled:
|
||||
raise ValueError("该类别被管理员强制为启用/禁用,无法修改")
|
||||
self.tool_category_states[category] = bool(enabled)
|
||||
final_enabled = bool(enabled)
|
||||
self.tool_category_states[category] = final_enabled
|
||||
self._ensure_runtime_tool_overrides()[category] = final_enabled
|
||||
self._refresh_disabled_tools()
|
||||
|
||||
def set_admin_policy(
|
||||
@ -229,10 +261,12 @@ class MainTerminalToolsPolicyMixin:
|
||||
default_mcp_cat = build_default_mcp_category()
|
||||
self.tool_categories_map["mcp"] = type(next(iter(TOOL_CATEGORIES.values())))(
|
||||
label=default_mcp_cat["label"],
|
||||
tools=[],
|
||||
tools=["list_mcp_servers"],
|
||||
default_enabled=True,
|
||||
silent_when_disabled=False,
|
||||
)
|
||||
elif "mcp" in self.tool_categories_map:
|
||||
self.tool_categories_map["mcp"].tools = ["list_mcp_servers"]
|
||||
# 重新构建启用状态映射,保留已有值
|
||||
new_states: Dict[str, bool] = {}
|
||||
for key, cat in self.tool_categories_map.items():
|
||||
@ -248,6 +282,8 @@ class MainTerminalToolsPolicyMixin:
|
||||
|
||||
self.admin_forced_category_states = forced_category_states or {}
|
||||
self.admin_disabled_models = disabled_models or []
|
||||
self._prune_runtime_tool_overrides()
|
||||
self._apply_runtime_tool_overrides()
|
||||
self._refresh_disabled_tools()
|
||||
|
||||
def get_tool_settings_snapshot(self) -> List[Dict[str, object]]:
|
||||
|
||||
@ -172,13 +172,23 @@ def _collect_categories_with_overrides(overrides: Dict[str, Any]) -> Dict[str, D
|
||||
# 注入 MCP 工具分类(动态)
|
||||
mcp_cat = build_default_mcp_category()
|
||||
mcp_cat_id = mcp_cat.get("id", "mcp")
|
||||
mcp_tools = ["list_mcp_servers", *mcp_registry.build_cached_alias_list()]
|
||||
base[mcp_cat_id] = {
|
||||
"label": mcp_cat.get("label", "MCP 工具"),
|
||||
"tools": mcp_tools,
|
||||
"tools": ["list_mcp_servers"],
|
||||
"default_enabled": True,
|
||||
"silent_when_disabled": False,
|
||||
}
|
||||
for server in mcp_registry.list_enabled_servers():
|
||||
sid = str(server.get("id") or "").strip()
|
||||
if not sid:
|
||||
continue
|
||||
cat_id = MCPServerRegistry.make_server_category_id(sid)
|
||||
base[cat_id] = {
|
||||
"label": MCPServerRegistry.make_server_category_label(server),
|
||||
"tools": mcp_registry.build_cached_alias_list(server_id=sid),
|
||||
"default_enabled": True,
|
||||
"silent_when_disabled": False,
|
||||
}
|
||||
|
||||
remove_ids = set(overrides.get("remove_categories") or [])
|
||||
for rid in remove_ids:
|
||||
@ -200,6 +210,33 @@ def _collect_categories_with_overrides(overrides: Dict[str, Any]) -> Dict[str, D
|
||||
"default_enabled": default_enabled,
|
||||
"silent_when_disabled": bool(payload.get("silent_when_disabled", False)),
|
||||
}
|
||||
|
||||
# MCP 分类由运行时动态决定:基础 mcp 仅保留原生 list_mcp_servers,
|
||||
# 每个 mcp_server__* 分类独立承载该服务别名工具,避免旧策略覆盖造成错乱。
|
||||
base["mcp"] = {
|
||||
**(base.get("mcp") or {}),
|
||||
"label": (base.get("mcp") or {}).get("label") or mcp_cat.get("label", "MCP 工具"),
|
||||
"tools": ["list_mcp_servers"],
|
||||
"default_enabled": bool((base.get("mcp") or {}).get("default_enabled", True)),
|
||||
"silent_when_disabled": bool((base.get("mcp") or {}).get("silent_when_disabled", False)),
|
||||
}
|
||||
active_server_categories = set()
|
||||
for server in mcp_registry.list_enabled_servers():
|
||||
sid = str(server.get("id") or "").strip()
|
||||
if not sid:
|
||||
continue
|
||||
cat_id = MCPServerRegistry.make_server_category_id(sid)
|
||||
active_server_categories.add(cat_id)
|
||||
existing = base.get(cat_id) or {}
|
||||
base[cat_id] = {
|
||||
"label": MCPServerRegistry.make_server_category_label(server),
|
||||
"tools": mcp_registry.build_cached_alias_list(server_id=sid),
|
||||
"default_enabled": bool(existing.get("default_enabled", True)),
|
||||
"silent_when_disabled": bool(existing.get("silent_when_disabled", False)),
|
||||
}
|
||||
for cid in list(base.keys()):
|
||||
if isinstance(cid, str) and cid.startswith("mcp_server__") and cid not in active_server_categories:
|
||||
base.pop(cid, None)
|
||||
return base
|
||||
|
||||
|
||||
@ -267,10 +304,21 @@ def describe_defaults() -> Dict[str, Any]:
|
||||
mcp_cat_id = mcp_cat.get("id", "mcp")
|
||||
categories[mcp_cat_id] = {
|
||||
"label": mcp_cat.get("label", "MCP 工具"),
|
||||
"tools": ["list_mcp_servers", *mcp_registry.build_cached_alias_list()],
|
||||
"tools": ["list_mcp_servers"],
|
||||
"default_enabled": True,
|
||||
"silent_when_disabled": False,
|
||||
}
|
||||
for server in mcp_registry.list_enabled_servers():
|
||||
sid = str(server.get("id") or "").strip()
|
||||
if not sid:
|
||||
continue
|
||||
cat_id = MCPServerRegistry.make_server_category_id(sid)
|
||||
categories[cat_id] = {
|
||||
"label": MCPServerRegistry.make_server_category_label(server),
|
||||
"tools": mcp_registry.build_cached_alias_list(server_id=sid),
|
||||
"default_enabled": True,
|
||||
"silent_when_disabled": False,
|
||||
}
|
||||
|
||||
return {
|
||||
"categories": categories,
|
||||
|
||||
@ -106,6 +106,24 @@ class MCPServerRegistry:
|
||||
remain = max(8, max_len - len(prefix))
|
||||
return f"{prefix}{tid[:remain]}"
|
||||
|
||||
@classmethod
|
||||
def make_server_category_id(cls, server_id: str, *, max_len: int = 64) -> str:
|
||||
sid = cls._safe_slug(server_id, "server")
|
||||
prefix = "mcp_server__"
|
||||
remain = max(8, max_len - len(prefix))
|
||||
return f"{prefix}{sid[:remain]}"
|
||||
|
||||
@classmethod
|
||||
def make_server_category_label(cls, server: Any) -> str:
|
||||
if isinstance(server, dict):
|
||||
name = str(server.get("name") or server.get("id") or "").strip()
|
||||
sid = str(server.get("id") or "").strip()
|
||||
else:
|
||||
name = str(server or "").strip()
|
||||
sid = ""
|
||||
target = name or sid or "未命名服务"
|
||||
return f"MCP · {target}"
|
||||
|
||||
@staticmethod
|
||||
def build_default_mcp_category() -> Dict[str, Any]:
|
||||
return {
|
||||
|
||||
@ -605,11 +605,15 @@ def _sanitize_tool_categories(value: Any, allowed: set) -> list:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
result = []
|
||||
dynamic_prefixes = ("mcp_server__",)
|
||||
for item in value:
|
||||
if not isinstance(item, str):
|
||||
continue
|
||||
candidate = item.strip()
|
||||
if not candidate or candidate not in allowed:
|
||||
if not candidate:
|
||||
continue
|
||||
dynamic_ok = candidate == "custom" or any(candidate.startswith(prefix) for prefix in dynamic_prefixes)
|
||||
if candidate not in allowed and not dynamic_ok:
|
||||
continue
|
||||
if candidate not in result:
|
||||
result.append(candidate)
|
||||
|
||||
@ -149,6 +149,47 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
|
||||
terminal.user_role = "admin"
|
||||
if has_request_context():
|
||||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||||
|
||||
# 宿主机模式同样需要应用管理员策略(否则前端工具菜单会退化成静态基础分类)
|
||||
try:
|
||||
from core.tool_config import ToolCategory
|
||||
from modules import admin_policy_manager
|
||||
|
||||
record = get_current_user_record()
|
||||
role = get_current_user_role(record) if record else "admin"
|
||||
invite_code = getattr(record, "invite_code", None) if record else None
|
||||
policy = admin_policy_manager.get_effective_policy(
|
||||
record.username if record else username,
|
||||
role,
|
||||
invite_code,
|
||||
)
|
||||
categories_map = {
|
||||
cid: ToolCategory(
|
||||
label=cat.get("label") or cid,
|
||||
tools=list(cat.get("tools") or []),
|
||||
default_enabled=bool(cat.get("default_enabled", True)),
|
||||
silent_when_disabled=bool(cat.get("silent_when_disabled", False)),
|
||||
)
|
||||
for cid, cat in policy.get("categories", {}).items()
|
||||
}
|
||||
forced_states = policy.get("forced_category_states") or {}
|
||||
disabled_models = policy.get("disabled_models") or []
|
||||
terminal.set_admin_policy(categories_map, forced_states, disabled_models)
|
||||
terminal.admin_policy_ui_blocks = policy.get("ui_blocks") or {}
|
||||
terminal.admin_policy_version = policy.get("updated_at")
|
||||
if terminal.model_key in disabled_models:
|
||||
for candidate in get_registered_model_keys(visible_only=True):
|
||||
if candidate not in disabled_models:
|
||||
try:
|
||||
terminal.set_model(candidate)
|
||||
if has_request_context():
|
||||
session["model_key"] = terminal.model_key
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as exc:
|
||||
debug_log(f"[admin_policy][host_mode] 应用失败: {exc}")
|
||||
|
||||
_ensure_workspace_skills_synced(terminal, workspace)
|
||||
return terminal, workspace
|
||||
|
||||
|
||||
@ -17,6 +17,9 @@ import { debugLog } from './common';
|
||||
|
||||
export const toolingMethods = {
|
||||
toolCategoryIcon(categoryId) {
|
||||
if (typeof categoryId === 'string' && categoryId.startsWith('mcp_server__')) {
|
||||
return this.toolCategoryIcons.mcp || 'settings';
|
||||
}
|
||||
return this.toolCategoryIcons[categoryId] || 'settings';
|
||||
},
|
||||
|
||||
@ -142,13 +145,34 @@ export const toolingMethods = {
|
||||
locked: !!item.locked,
|
||||
locked_state: typeof item.locked_state === 'boolean' ? item.locked_state : null
|
||||
}));
|
||||
|
||||
let ordered = normalized;
|
||||
const mcpServerCategories = normalized
|
||||
.filter((item) => typeof item.id === 'string' && item.id.startsWith('mcp_server__'))
|
||||
.sort((a, b) => String(a.label || '').localeCompare(String(b.label || '')));
|
||||
if (mcpServerCategories.length > 0) {
|
||||
const nonMcpServer = normalized.filter(
|
||||
(item) => !(typeof item.id === 'string' && item.id.startsWith('mcp_server__'))
|
||||
);
|
||||
const mcpIndex = nonMcpServer.findIndex((item) => item.id === 'mcp');
|
||||
if (mcpIndex >= 0) {
|
||||
ordered = [
|
||||
...nonMcpServer.slice(0, mcpIndex + 1),
|
||||
...mcpServerCategories,
|
||||
...nonMcpServer.slice(mcpIndex + 1)
|
||||
];
|
||||
} else {
|
||||
ordered = [...mcpServerCategories, ...nonMcpServer];
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('[ToolSettings] Snapshot applied', {
|
||||
received: categories.length,
|
||||
normalized,
|
||||
anyEnabled: normalized.some((cat) => cat.enabled),
|
||||
toolExamples: normalized.slice(0, 3)
|
||||
normalized: ordered,
|
||||
anyEnabled: ordered.some((cat) => cat.enabled),
|
||||
toolExamples: ordered.slice(0, 3)
|
||||
});
|
||||
this.toolSetSettings(normalized);
|
||||
this.toolSetSettings(ordered);
|
||||
this.toolSetSettingsLoading(false);
|
||||
},
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user