270 lines
12 KiB
Python
270 lines
12 KiB
Python
# core/main_terminal.py - 主终端(集成对话持久化)
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Set, TYPE_CHECKING
|
|
|
|
try:
|
|
from config import (
|
|
OUTPUT_FORMATS,
|
|
DATA_DIR,
|
|
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_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[1]
|
|
if str(project_root) not in sys.path:
|
|
sys.path.insert(0, str(project_root))
|
|
from config import (
|
|
OUTPUT_FORMATS,
|
|
DATA_DIR,
|
|
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_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_manager import SubAgentManager
|
|
from modules.ocr_client import OCRClient
|
|
from modules.easter_egg_manager import EasterEggManager
|
|
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
|
|
from modules.custom_tool_executor import CustomToolExecutor
|
|
|
|
try:
|
|
from config.limits import THINKING_FAST_INTERVAL
|
|
except ImportError:
|
|
THINKING_FAST_INTERVAL = 10
|
|
|
|
from core.tool_config import TOOL_CATEGORIES
|
|
from utils.api_client import DeepSeekClient
|
|
from utils.context_manager import ContextManager
|
|
from config.model_profiles import get_model_profile
|
|
|
|
from core.main_terminal_parts import (
|
|
MainTerminalCommandMixin,
|
|
MainTerminalContextMixin,
|
|
MainTerminalToolsMixin,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from modules.user_container_manager import ContainerHandle
|
|
|
|
|
|
class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTerminalToolsMixin):
|
|
def __init__(
|
|
self,
|
|
project_path: str,
|
|
thinking_mode: bool = False,
|
|
run_mode: Optional[str] = None,
|
|
data_dir: Optional[str] = None,
|
|
container_session: Optional["ContainerHandle"] = None,
|
|
usage_tracker: Optional[object] = None,
|
|
):
|
|
self.project_path = project_path
|
|
allowed_modes = {"fast", "thinking", "deep"}
|
|
initial_mode = run_mode if run_mode in allowed_modes else ("thinking" if thinking_mode else "fast")
|
|
self.run_mode = initial_mode
|
|
self.thinking_mode = initial_mode != "fast" # False=快速模式, True=思考模式
|
|
self.deep_thinking_mode = initial_mode == "deep"
|
|
self.data_dir = Path(data_dir).expanduser().resolve() if data_dir else Path(DATA_DIR).resolve()
|
|
self.usage_tracker = usage_tracker
|
|
|
|
# 初始化组件
|
|
self.api_client = DeepSeekClient(thinking_mode=self.thinking_mode)
|
|
self.api_client.project_path = project_path
|
|
self.api_client.set_deep_thinking_mode(self.deep_thinking_mode)
|
|
self.model_key = "kimi-k2.5"
|
|
self.model_profile = get_model_profile(self.model_key)
|
|
self.apply_model_profile(self.model_profile)
|
|
self.context_manager = ContextManager(project_path, data_dir=str(self.data_dir))
|
|
self.context_manager.main_terminal = self
|
|
self.container_mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace"
|
|
self.container_cpu_limit = TERMINAL_SANDBOX_CPUS or "未限制"
|
|
self.container_memory_limit = TERMINAL_SANDBOX_MEMORY or "未限制"
|
|
self.project_storage_limit = f"{PROJECT_MAX_STORAGE_MB}MB" if PROJECT_MAX_STORAGE_MB else "未限制"
|
|
self.project_storage_limit_bytes = (
|
|
PROJECT_MAX_STORAGE_MB * 1024 * 1024 if PROJECT_MAX_STORAGE_MB else None
|
|
)
|
|
self.container_session: Optional["ContainerHandle"] = None
|
|
self.memory_manager = MemoryManager(data_dir=str(self.data_dir))
|
|
self.file_manager = FileManager(project_path, container_session=container_session)
|
|
self.search_engine = SearchEngine()
|
|
self.terminal_ops = TerminalOperator(project_path, container_session=container_session)
|
|
self.ocr_client = OCRClient(project_path, self.file_manager)
|
|
self.pending_image_view = None # 供 view_image 工具使用,保存一次性图片附加请求
|
|
self.pending_video_view = None # 供 view_video 工具使用,保存一次性视频附加请求
|
|
|
|
# 新增:终端管理器
|
|
self.terminal_manager = TerminalManager(
|
|
project_path=project_path,
|
|
max_terminals=MAX_TERMINALS,
|
|
terminal_buffer_size=TERMINAL_BUFFER_SIZE,
|
|
terminal_display_size=TERMINAL_DISPLAY_SIZE,
|
|
broadcast_callback=None, # CLI模式不需要广播
|
|
container_session=container_session,
|
|
)
|
|
# 让 run_command/run_python 复用终端容器,保持环境一致
|
|
self.terminal_ops.attach_terminal_manager(self.terminal_manager)
|
|
self._apply_container_session(container_session)
|
|
|
|
self.todo_manager = TodoManager(self.context_manager)
|
|
self.sub_agent_manager = SubAgentManager(
|
|
project_path=self.project_path,
|
|
data_dir=str(self.data_dir)
|
|
)
|
|
self.easter_egg_manager = EasterEggManager()
|
|
self._announced_sub_agent_tasks = set()
|
|
self.silent_tool_disable = False # 是否静默工具禁用提示
|
|
self.current_session_id = 0 # 用于标识不同的任务会话
|
|
# 工具类别(可被管理员动态覆盖)
|
|
self.tool_categories_map = dict(TOOL_CATEGORIES)
|
|
# 自定义工具仅管理员可见/可用
|
|
self.user_role: str = "user"
|
|
self.custom_tools_enabled = bool(CUSTOM_TOOLS_ENABLED)
|
|
self.custom_tool_registry = CustomToolRegistry()
|
|
self.custom_tool_executor = CustomToolExecutor(self.custom_tool_registry, self.terminal_ops)
|
|
if self.custom_tools_enabled:
|
|
default_custom_cat = build_default_tool_category()
|
|
# 若未存在 custom 分类则添加,方便前端/策略统一展示
|
|
if "custom" not in self.tool_categories_map:
|
|
self.tool_categories_map["custom"] = type(next(iter(TOOL_CATEGORIES.values())))(
|
|
label=default_custom_cat["label"],
|
|
tools=default_custom_cat["tools"],
|
|
default_enabled=True,
|
|
silent_when_disabled=False,
|
|
)
|
|
self.admin_forced_category_states: Dict[str, Optional[bool]] = {}
|
|
self.admin_disabled_models: List[str] = []
|
|
self.admin_policy_ui_blocks: Dict[str, bool] = {}
|
|
self.admin_policy_version: Optional[str] = None
|
|
|
|
# 工具启用状态
|
|
self.tool_category_states: Dict[str, bool] = {
|
|
key: category.default_enabled
|
|
for key, category in self.tool_categories_map.items()
|
|
}
|
|
self.disabled_tools: Set[str] = set()
|
|
self.disabled_notice_tools: Set[str] = set()
|
|
self._refresh_disabled_tools()
|
|
self.thinking_fast_interval = THINKING_FAST_INTERVAL
|
|
self.default_disabled_tool_categories: List[str] = []
|
|
# 个性化工具意图开关
|
|
self.tool_intent_enabled: bool = False
|
|
self.apply_personalization_preferences()
|
|
|
|
# 新增:自动开始新对话
|
|
self._ensure_conversation()
|
|
|
|
# 命令映射
|
|
self.commands = {
|
|
"help": self.show_help,
|
|
"exit": self.exit_system,
|
|
"status": self.show_status,
|
|
"memory": self.manage_memory,
|
|
"clear": self.clear_conversation,
|
|
"history": self.show_history,
|
|
"files": self.show_files,
|
|
"mode": self.toggle_mode,
|
|
"terminals": self.show_terminals,
|
|
# 新增:对话管理命令
|
|
"conversations": self.show_conversations,
|
|
"load": self.load_conversation_command,
|
|
"new": self.new_conversation_command,
|
|
"save": self.save_conversation_command
|
|
}
|
|
|
|
def _apply_container_session(self, session: Optional["ContainerHandle"]):
|
|
self.container_session = session
|
|
if session and session.mode == "docker":
|
|
self.container_mount_path = session.mount_path or (TERMINAL_SANDBOX_MOUNT_PATH or "/workspace")
|
|
else:
|
|
self.container_mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace"
|
|
|
|
def _is_host_mode(self) -> bool:
|
|
"""判定当前是否运行在宿主机模式,用于豁免配额等限制。"""
|
|
if self.container_session and getattr(self.container_session, "mode", None) != "docker":
|
|
return True
|
|
return (TERMINAL_SANDBOX_MODE or "").lower() == "host"
|
|
|
|
def record_model_call(self, is_thinking: bool):
|
|
if self._is_host_mode():
|
|
return True, {}
|
|
tracker = getattr(self, "usage_tracker", None)
|
|
if not tracker:
|
|
return True, {}
|
|
mode = "thinking" if is_thinking else "fast"
|
|
try:
|
|
allowed, info = tracker.check_and_increment(mode)
|
|
if allowed:
|
|
self._notify_quota_update(mode)
|
|
return allowed, info
|
|
except Exception:
|
|
return True, {}
|
|
|
|
def record_search_call(self):
|
|
if self._is_host_mode():
|
|
return True, {}
|
|
tracker = getattr(self, "usage_tracker", None)
|
|
if not tracker:
|
|
return True, {}
|
|
try:
|
|
allowed, info = tracker.check_and_increment("search")
|
|
if allowed:
|
|
self._notify_quota_update("search")
|
|
return allowed, info
|
|
except Exception:
|
|
return True, {}
|
|
|
|
def _notify_quota_update(self, metric: str):
|
|
callback = getattr(self, "quota_update_callback", None)
|
|
if callable(callback):
|
|
try:
|
|
callback(metric)
|
|
except Exception:
|
|
pass
|
|
|
|
def update_container_session(self, session: Optional["ContainerHandle"]):
|
|
self._apply_container_session(session)
|
|
if getattr(self, "terminal_manager", None):
|
|
self.terminal_manager.update_container_session(session)
|
|
if getattr(self, "terminal_ops", None):
|
|
self.terminal_ops.set_container_session(session)
|
|
if getattr(self, "file_manager", None):
|
|
self.file_manager.set_container_session(session)
|
|
|
|
def _ensure_conversation(self):
|
|
"""确保CLI模式下存在可用的对话ID"""
|
|
if self.context_manager.current_conversation_id:
|
|
return
|
|
|
|
latest_list = self.context_manager.get_conversation_list(limit=1, offset=0)
|
|
conversations = latest_list.get("conversations", []) if latest_list else []
|
|
|
|
if conversations:
|
|
latest = conversations[0]
|
|
conv_id = latest.get("id")
|
|
if conv_id and self.context_manager.load_conversation_by_id(conv_id):
|
|
print(f"{OUTPUT_FORMATS['info']} 已加载最近对话: {conv_id}")
|
|
return
|
|
|
|
conversation_id = self.context_manager.start_new_conversation(
|
|
project_path=self.project_path,
|
|
thinking_mode=self.thinking_mode,
|
|
run_mode=self.run_mode
|
|
)
|
|
print(f"{OUTPUT_FORMATS['info']} 新建对话: {conversation_id}")
|