- 新增 modules/i18n.py:tr() + 进程级语言缓存 + modules/i18n_messages/ 域文案包自动聚合 - 新增 29 个域文案包,共 1153 条双语 key;90+ 源文件 1146 处用户可见消息 tr 化 - ui_locale 存入 personalization.json(用户级共享),前后端双向同步 - 前端匹配点双语兼容(history/shared/ChatArea/taskPolling/upload 等正则) - 修复语言判等陷阱:审批等待加稳定 code 字段;conversation.py 不存在判等改双语 helper - 边界:日志/prompt 注入/子智能体工具回填/容器内嵌脚本不迁移
541 lines
29 KiB
Python
541 lines
29 KiB
Python
import asyncio
|
||
import json
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional, Set
|
||
|
||
try:
|
||
from config import (
|
||
OUTPUT_FORMATS, DATA_DIR, PROMPTS_DIR, NEED_CONFIRMATION,
|
||
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE,
|
||
MAX_READ_FILE_CHARS, READ_TOOL_DEFAULT_MAX_CHARS,
|
||
READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER,
|
||
READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER,
|
||
READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES,
|
||
READ_TOOL_MAX_FILE_SIZE,
|
||
TERMINAL_SANDBOX_MOUNT_PATH,
|
||
TERMINAL_SANDBOX_MODE,
|
||
TERMINAL_SANDBOX_CPUS,
|
||
TERMINAL_SANDBOX_MEMORY,
|
||
PROJECT_MAX_STORAGE_MB,
|
||
CUSTOM_TOOLS_ENABLED,
|
||
)
|
||
except ImportError:
|
||
import sys
|
||
project_root = Path(__file__).resolve().parents[2]
|
||
if str(project_root) not in sys.path:
|
||
sys.path.insert(0, str(project_root))
|
||
from config import (
|
||
OUTPUT_FORMATS, DATA_DIR, PROMPTS_DIR, NEED_CONFIRMATION,
|
||
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE,
|
||
MAX_READ_FILE_CHARS, READ_TOOL_DEFAULT_MAX_CHARS,
|
||
READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER,
|
||
READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER,
|
||
READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES,
|
||
READ_TOOL_MAX_FILE_SIZE,
|
||
TERMINAL_SANDBOX_MOUNT_PATH,
|
||
TERMINAL_SANDBOX_MODE,
|
||
TERMINAL_SANDBOX_CPUS,
|
||
TERMINAL_SANDBOX_MEMORY,
|
||
PROJECT_MAX_STORAGE_MB,
|
||
CUSTOM_TOOLS_ENABLED,
|
||
)
|
||
|
||
from modules.file_manager import FileManager
|
||
from modules.search_engine import SearchEngine
|
||
from modules.terminal_ops import TerminalOperator
|
||
from modules.memory_manager import MemoryManager
|
||
from modules.terminal_manager import TerminalManager
|
||
from modules.todo_manager import TodoManager
|
||
from modules.sub_agent import SubAgentManager
|
||
from modules.webpage_extractor import extract_webpage_content, tavily_extract
|
||
from modules.ocr_client import OCRClient
|
||
from modules.easter_egg_manager import EasterEggManager
|
||
from modules.personalization_manager import (
|
||
load_personalization_config,
|
||
build_personalization_prompt,
|
||
)
|
||
from modules.skills_manager import (
|
||
get_skills_catalog,
|
||
build_skills_list,
|
||
merge_enabled_skills,
|
||
build_skills_prompt,
|
||
infer_private_skills_dir,
|
||
)
|
||
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
|
||
|
||
try:
|
||
from config.limits import REASONING_EFFORT_LEVELS
|
||
except ImportError:
|
||
REASONING_EFFORT_LEVELS = ("low", "medium", "high", "xhigh", "max")
|
||
|
||
from modules.container_monitor import collect_stats, inspect_state
|
||
from core.tool_config import TOOL_CATEGORIES
|
||
from utils.api_client import APIClient
|
||
from utils.context_manager import ContextManager
|
||
from utils.tool_result_formatter import format_tool_result_for_context
|
||
from utils.logger import setup_logger
|
||
from config.model_profiles import (
|
||
get_model_profile,
|
||
get_model_prompt_replacements,
|
||
get_model_context_window,
|
||
)
|
||
|
||
from modules.i18n import tr
|
||
|
||
logger = setup_logger(__name__)
|
||
DISABLE_LENGTH_CHECK = True
|
||
PERMISSION_MODES = {"readonly", "approval", "auto_approval", "unrestricted"}
|
||
# 运行模式(交互方式档):计划 / 询问 / 执行。
|
||
# 与权限模式(能力档)正交;plan 档会联动锁死权限为只读。
|
||
WORK_MODES = {"plan", "ask", "execute"}
|
||
WORK_MODE_DEFAULT = "plan"
|
||
|
||
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_default_model: bool = True, apply_default_modes: bool = True):
|
||
"""Apply persisted personalization settings that affect runtime behavior.
|
||
|
||
apply_default_modes=False 时跳过 default_model / default_run_mode /
|
||
default_reasoning_effort 三项默认值应用——对话加载链路必须传 False,
|
||
三者以对话 meta 为权威,默认值仅用于创建空对话。
|
||
"""
|
||
try:
|
||
effective_config = config or load_personalization_config(self.data_dir)
|
||
except Exception:
|
||
effective_config = {}
|
||
|
||
# 工具意图开关
|
||
self.tool_intent_enabled = bool(effective_config.get("tool_intent_enabled"))
|
||
# Skill 强约束开关
|
||
self.skill_strict_terminal_enabled = bool(effective_config.get("skill_strict_terminal_enabled", False))
|
||
self.skill_strict_sub_agent_enabled = bool(effective_config.get("skill_strict_sub_agent_enabled", False))
|
||
self.skill_strict_run_command_foreground_enabled = bool(
|
||
effective_config.get("skill_strict_run_command_foreground_enabled", False)
|
||
)
|
||
self.skill_strict_run_command_background_enabled = bool(
|
||
effective_config.get("skill_strict_run_command_background_enabled", False)
|
||
)
|
||
|
||
# 解析当前启用的 skills(用于强约束“仅对已启用 skill 生效”)
|
||
try:
|
||
skills_catalog = get_skills_catalog(private_dir=infer_private_skills_dir(self.data_dir))
|
||
enabled_skills = merge_enabled_skills(
|
||
effective_config.get("enabled_skills") if isinstance(effective_config, dict) else None,
|
||
skills_catalog,
|
||
effective_config.get("skills_catalog_snapshot") if isinstance(effective_config, dict) else None,
|
||
)
|
||
self.enabled_skill_ids = set(enabled_skills or [])
|
||
except Exception:
|
||
self.enabled_skill_ids = set()
|
||
|
||
disabled_categories = []
|
||
raw_disabled = effective_config.get("disabled_tool_categories")
|
||
if isinstance(raw_disabled, list):
|
||
disabled_categories = [
|
||
key for key in raw_disabled
|
||
if isinstance(key, str) and key in self.tool_categories_map
|
||
]
|
||
self.default_disabled_tool_categories = disabled_categories
|
||
|
||
# 图片压缩模式传递给上下文
|
||
img_mode = effective_config.get("image_compression")
|
||
if isinstance(img_mode, str):
|
||
self.context_manager.image_compression_mode = img_mode
|
||
|
||
# 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()
|
||
|
||
# 默认模型偏好(优先应用,再处理运行模式)
|
||
preferred_model = effective_config.get("default_model")
|
||
if apply_default_modes and apply_default_model and isinstance(preferred_model, str) and preferred_model != self.model_key:
|
||
try:
|
||
self.set_model(preferred_model)
|
||
except Exception as exc:
|
||
logger.warning("忽略无效默认模型: %s (%s)", preferred_model, exc)
|
||
|
||
preferred_mode = effective_config.get("default_run_mode")
|
||
if apply_default_modes and isinstance(preferred_mode, str):
|
||
normalized_mode = preferred_mode.strip().lower()
|
||
if normalized_mode == "deep": # 旧版标识符映射
|
||
normalized_mode = "thinking"
|
||
if normalized_mode in {"fast", "thinking"} and normalized_mode != self.run_mode:
|
||
try:
|
||
self.set_run_mode(normalized_mode)
|
||
except ValueError:
|
||
logger.warning("忽略无效默认运行模式: %s", preferred_mode)
|
||
|
||
# 默认推理强度(None=默认,不传参)
|
||
if apply_default_modes:
|
||
preferred_effort = effective_config.get("default_reasoning_effort")
|
||
if isinstance(preferred_effort, str):
|
||
preferred_effort = preferred_effort.strip().lower() or None
|
||
if preferred_effort not in REASONING_EFFORT_LEVELS:
|
||
preferred_effort = None
|
||
else:
|
||
preferred_effort = None
|
||
try:
|
||
self.set_reasoning_effort(preferred_effort)
|
||
except (ValueError, AttributeError):
|
||
pass
|
||
|
||
# 静默禁用工具提示
|
||
self.silent_tool_disable = bool(effective_config.get("silent_tool_disable"))
|
||
permission_mode = effective_config.get("default_permission_mode")
|
||
if isinstance(permission_mode, str) and permission_mode in PERMISSION_MODES:
|
||
self.default_permission_mode = permission_mode
|
||
else:
|
||
self.default_permission_mode = "unrestricted"
|
||
if not getattr(self, "current_permission_mode", None):
|
||
self.current_permission_mode = self.default_permission_mode
|
||
work_mode = effective_config.get("default_work_mode")
|
||
if isinstance(work_mode, str) and work_mode.strip().lower() in WORK_MODES:
|
||
self.default_work_mode = work_mode.strip().lower()
|
||
else:
|
||
self.default_work_mode = WORK_MODE_DEFAULT
|
||
if not getattr(self, "current_work_mode", None):
|
||
self.current_work_mode = self.default_work_mode
|
||
|
||
def get_permission_mode(self) -> str:
|
||
mode = str(getattr(self, "current_permission_mode", "unrestricted") or "unrestricted")
|
||
if mode not in PERMISSION_MODES:
|
||
return "unrestricted"
|
||
return mode
|
||
|
||
def get_work_mode(self) -> str:
|
||
mode = str(getattr(self, "current_work_mode", None) or WORK_MODE_DEFAULT).strip().lower()
|
||
if mode not in WORK_MODES:
|
||
return WORK_MODE_DEFAULT
|
||
return mode
|
||
|
||
def set_work_mode(self, mode: str, *, persist: bool = True, conversation_id: Optional[str] = None) -> str:
|
||
normalized = str(mode or "").strip().lower()
|
||
if normalized not in WORK_MODES:
|
||
raise ValueError(tr("tools_policy.invalid_work_mode"))
|
||
self.current_work_mode = normalized
|
||
if not persist:
|
||
return normalized
|
||
|
||
conv_id = conversation_id or getattr(getattr(self, "context_manager", None), "current_conversation_id", None)
|
||
if conv_id and getattr(self, "context_manager", None):
|
||
try:
|
||
self.context_manager._get_conversation_manager_for_id(conv_id).update_conversation_metadata(conv_id,
|
||
{"work_mode": normalized},
|
||
)
|
||
if self.context_manager.current_conversation_id == conv_id:
|
||
self.context_manager.conversation_metadata["work_mode"] = normalized
|
||
except Exception:
|
||
pass
|
||
return normalized
|
||
|
||
def switch_work_mode(self, mode: str, *, persist: bool = True, conversation_id: Optional[str] = None) -> Dict[str, Any]:
|
||
"""切换运行模式(plan/ask/execute),并处理 plan ⇄ 只读权限的联动。
|
||
|
||
- 进入 plan:先把当前权限模式存为 pre_plan_permission_mode,再强制只读;
|
||
- 离开 plan:恢复 pre_plan_permission_mode(无记录则不动当前权限);
|
||
- ask / execute 之间切换不动权限。
|
||
返回 {"mode": ..., "permission_mode": ...} 供调用方做通知与状态广播。
|
||
"""
|
||
normalized = str(mode or "").strip().lower()
|
||
if normalized not in WORK_MODES:
|
||
raise ValueError(tr("tools_policy.invalid_work_mode"))
|
||
previous = self.get_work_mode()
|
||
conv_id = conversation_id or getattr(getattr(self, "context_manager", None), "current_conversation_id", None)
|
||
|
||
entering_plan = normalized == "plan" and previous != "plan"
|
||
leaving_plan = previous == "plan" and normalized != "plan"
|
||
|
||
if entering_plan:
|
||
current_permission = self.get_permission_mode()
|
||
if current_permission != "readonly":
|
||
# 记录进入前的权限模式,供离开 plan 时恢复(Claude Code prePlanMode 式)
|
||
if persist:
|
||
try:
|
||
self._persist_runtime_mode_metadata({"pre_plan_permission_mode": current_permission})
|
||
except Exception:
|
||
pass
|
||
self.set_permission_mode("readonly", persist=persist, conversation_id=conv_id)
|
||
# 执行环境联动:direct ⇒ sandbox。只读权限在宿主机依赖 OS 沙箱硬限制,
|
||
# direct(完全访问)下无沙箱,只读形同虚设,必须一并锁回沙箱。
|
||
try:
|
||
if hasattr(self, "get_execution_mode") and self.get_execution_mode() == "direct":
|
||
if persist:
|
||
try:
|
||
self._persist_runtime_mode_metadata({"pre_plan_execution_mode": "direct"})
|
||
except Exception:
|
||
pass
|
||
if hasattr(self, "set_execution_mode"):
|
||
self.set_execution_mode("sandbox")
|
||
if persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||
try:
|
||
self._persist_runtime_mode_metadata({"execution_mode": "sandbox"})
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
# 先落新模式,再恢复权限:set_permission_mode 的 plan 锁以新模式为准
|
||
self.set_work_mode(normalized, persist=persist, conversation_id=conv_id)
|
||
if leaving_plan:
|
||
restore = None
|
||
try:
|
||
meta = getattr(getattr(self, "context_manager", None), "conversation_metadata", None) or {}
|
||
restore = str(meta.get("pre_plan_permission_mode") or "").strip().lower() or None
|
||
except Exception:
|
||
restore = None
|
||
if restore not in PERMISSION_MODES:
|
||
restore = None
|
||
if not restore:
|
||
# 无记录(如新对话直接以 plan 创建):回落到个性化默认权限模式
|
||
try:
|
||
prefs = load_personalization_config(getattr(self, "data_dir", None))
|
||
candidate = str(prefs.get("default_permission_mode") or "").strip().lower()
|
||
if candidate in PERMISSION_MODES:
|
||
restore = candidate
|
||
except Exception:
|
||
restore = None
|
||
if restore and restore != "readonly":
|
||
self.set_permission_mode(restore, persist=persist, conversation_id=conv_id)
|
||
if persist:
|
||
try:
|
||
self._persist_runtime_mode_metadata({"pre_plan_permission_mode": None})
|
||
except Exception:
|
||
pass
|
||
# 恢复执行环境(仅当有明确进入前记录;无记录保持 sandbox,安全默认)
|
||
try:
|
||
meta = getattr(getattr(self, "context_manager", None), "conversation_metadata", None) or {}
|
||
pre_exec = str(meta.get("pre_plan_execution_mode") or "").strip().lower()
|
||
if pre_exec == "direct" and hasattr(self, "set_execution_mode"):
|
||
self.set_execution_mode("direct")
|
||
if persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||
try:
|
||
self._persist_runtime_mode_metadata({"execution_mode": "direct", "pre_plan_execution_mode": None})
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
return {"mode": normalized, "previous_mode": previous, "permission_mode": self.get_permission_mode()}
|
||
|
||
def set_permission_mode(self, mode: str, *, persist: bool = True, conversation_id: Optional[str] = None) -> str:
|
||
normalized = str(mode or "").strip().lower()
|
||
if normalized not in PERMISSION_MODES:
|
||
raise ValueError(tr("tools_policy.invalid_permission_mode"))
|
||
# 计划模式下权限锁死为只读(后端强制):仅允许保持/切到 readonly。
|
||
# 解除锁定的唯一路径是 switch_work_mode 先切离 plan 再恢复权限。
|
||
if normalized != "readonly" and getattr(self, "get_work_mode", None):
|
||
try:
|
||
if self.get_work_mode() == "plan":
|
||
raise ValueError(tr("tools_policy.plan_mode_locks_readonly"))
|
||
except AttributeError:
|
||
pass
|
||
previous = self.get_permission_mode()
|
||
entering_readonly = normalized == "readonly" and previous != "readonly"
|
||
leaving_readonly = previous == "readonly" and normalized != "readonly"
|
||
self.current_permission_mode = normalized
|
||
if not persist:
|
||
# 只读联动不依赖持久化:运行中 pending 切换路径(apply_pending_runtime_mode_changes)
|
||
# 也要内存级强制沙箱,只是不落 metadata(无记录则切离时保持沙箱,安全默认)。
|
||
self._apply_readonly_execution_mode_link(entering_readonly, leaving_readonly, persist=False)
|
||
return normalized
|
||
|
||
conv_id = conversation_id or getattr(getattr(self, "context_manager", None), "current_conversation_id", None)
|
||
if conv_id and getattr(self, "context_manager", None):
|
||
try:
|
||
self.context_manager._get_conversation_manager_for_id(conv_id).update_conversation_metadata(conv_id,
|
||
{"permission_mode": normalized},
|
||
)
|
||
if self.context_manager.current_conversation_id == conv_id:
|
||
self.context_manager.conversation_metadata["permission_mode"] = normalized
|
||
except Exception:
|
||
pass
|
||
self._apply_readonly_execution_mode_link(entering_readonly, leaving_readonly, persist=True)
|
||
return normalized
|
||
|
||
def _apply_readonly_execution_mode_link(self, entering: bool, leaving: bool, *, persist: bool) -> None:
|
||
"""只读权限 ⇄ 执行环境联动:进入 readonly 强制切沙箱,切离恢复进入前执行环境。
|
||
|
||
与 plan ⇄ readonly+sandbox 双锁逻辑对称:只读权限在宿主机依赖 OS 沙箱硬限制,
|
||
direct(完全访问)下无沙箱,只读形同虚设,必须一并锁回沙箱。
|
||
进入 readonly 时若执行环境为 direct,先存 pre_readonly_execution_mode 供切离时恢复;
|
||
切离 readonly 时仅当有明确进入前记录才恢复(无记录保持 sandbox,安全默认)。
|
||
"""
|
||
if entering:
|
||
try:
|
||
if hasattr(self, "get_execution_mode") and self.get_execution_mode() == "direct":
|
||
if persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||
try:
|
||
self._persist_runtime_mode_metadata({"pre_readonly_execution_mode": "direct"})
|
||
except Exception:
|
||
pass
|
||
if hasattr(self, "set_execution_mode"):
|
||
self.set_execution_mode("sandbox")
|
||
if persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||
try:
|
||
self._persist_runtime_mode_metadata({"execution_mode": "sandbox"})
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
return
|
||
if leaving:
|
||
try:
|
||
meta = getattr(getattr(self, "context_manager", None), "conversation_metadata", None) or {}
|
||
pre_exec = str(meta.get("pre_readonly_execution_mode") or "").strip().lower()
|
||
except Exception:
|
||
pre_exec = ""
|
||
if pre_exec == "direct" and hasattr(self, "set_execution_mode"):
|
||
try:
|
||
self.set_execution_mode("direct")
|
||
if persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||
try:
|
||
self._persist_runtime_mode_metadata({"execution_mode": "direct", "pre_readonly_execution_mode": None})
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
elif persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||
try:
|
||
self._persist_runtime_mode_metadata({"pre_readonly_execution_mode": None})
|
||
except Exception:
|
||
pass
|
||
|
||
def set_tool_category_enabled(self, category: str, enabled: bool) -> None:
|
||
"""设置工具类别的启用状态 / Toggle tool category enablement."""
|
||
categories = self.tool_categories_map
|
||
if category not in categories:
|
||
raise ValueError(tr("tools_policy.unknown_tool_category", category=category))
|
||
forced = self.admin_forced_category_states.get(category)
|
||
if isinstance(forced, bool) and forced != enabled:
|
||
raise ValueError(tr("tools_policy.category_admin_locked"))
|
||
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(
|
||
self,
|
||
categories: Optional[Dict[str, "ToolCategory"]] = None,
|
||
forced_category_states: Optional[Dict[str, Optional[bool]]] = None,
|
||
disabled_models: Optional[List[str]] = None,
|
||
) -> None:
|
||
"""应用管理员策略(工具分类、强制开关、模型禁用)。"""
|
||
if categories:
|
||
self.tool_categories_map = dict(categories)
|
||
# 保证自定义工具分类存在(仅当功能启用)
|
||
if self.custom_tools_enabled and "custom" not in self.tool_categories_map:
|
||
self.tool_categories_map["custom"] = type(next(iter(TOOL_CATEGORIES.values())))(
|
||
label="自定义工具",
|
||
tools=[],
|
||
default_enabled=True,
|
||
silent_when_disabled=False,
|
||
)
|
||
if getattr(self, "mcp_tools_enabled", False) and "mcp" not in self.tool_categories_map:
|
||
default_mcp_cat = build_default_mcp_category()
|
||
self.tool_categories_map["mcp"] = type(next(iter(TOOL_CATEGORIES.values())))(
|
||
label=default_mcp_cat["label"],
|
||
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():
|
||
if key in self.tool_category_states:
|
||
new_states[key] = self.tool_category_states[key]
|
||
else:
|
||
new_states[key] = cat.default_enabled
|
||
self.tool_category_states = new_states
|
||
# 清理已被移除的类别
|
||
for removed in list(self.tool_category_states.keys()):
|
||
if removed not in self.tool_categories_map:
|
||
self.tool_category_states.pop(removed, None)
|
||
|
||
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]]:
|
||
"""获取工具类别状态快照 / Return tool category states snapshot."""
|
||
snapshot: List[Dict[str, object]] = []
|
||
categories = self.tool_categories_map
|
||
for key, category in categories.items():
|
||
forced = self.admin_forced_category_states.get(key)
|
||
enabled = self.tool_category_states.get(key, category.default_enabled)
|
||
if isinstance(forced, bool):
|
||
enabled = forced
|
||
snapshot.append({
|
||
"id": key,
|
||
"label": category.label,
|
||
"enabled": enabled,
|
||
"tools": list(category.tools),
|
||
"locked": isinstance(forced, bool),
|
||
"locked_state": forced if isinstance(forced, bool) else None,
|
||
})
|
||
return snapshot
|
||
|
||
def _refresh_disabled_tools(self) -> None:
|
||
"""刷新禁用工具列表 / Refresh disabled tool set."""
|
||
disabled: Set[str] = set()
|
||
notice: Set[str] = set()
|
||
categories = self.tool_categories_map
|
||
for key, category in categories.items():
|
||
state = self.tool_category_states.get(key, category.default_enabled)
|
||
forced = self.admin_forced_category_states.get(key)
|
||
if isinstance(forced, bool):
|
||
state = forced
|
||
if not state:
|
||
disabled.update(category.tools)
|
||
if not getattr(category, "silent_when_disabled", False):
|
||
notice.update(category.tools)
|
||
self.disabled_tools = disabled
|
||
self.disabled_notice_tools = notice
|
||
|
||
def _format_disabled_tool_notice(self) -> Optional[str]:
|
||
"""生成禁用工具提示信息 / Format disabled tool notice."""
|
||
if getattr(self, "silent_tool_disable", False):
|
||
return None
|
||
if not self.disabled_notice_tools:
|
||
return None
|
||
|
||
lines = ["=== 工具可用性提醒 ==="]
|
||
for tool_name in sorted(self.disabled_notice_tools):
|
||
lines.append(f"{tool_name}:已被用户禁用")
|
||
lines.append("=== 提示结束 ===")
|
||
return "\n".join(lines)
|