""" 连接管理器:保存多个 SSH 会话的配置和活跃连接。 配置持久化到 ~/.sshclient/hosts.json。 """ import json import os import threading from pathlib import Path from typing import Dict, List, Optional from .ssh_client import SSHConnection CONFIG_DIR = Path.home() / ".sshclient" CONFIG_FILE = CONFIG_DIR / "hosts.json" AI_CONFIG_FILE = CONFIG_DIR / "ai.json" class ConnectionManager: """多主机连接管理 + 配置持久化 + 分组""" def __init__(self): CONFIG_DIR.mkdir(parents=True, exist_ok=True) self._lock = threading.RLock() self.hosts: List[dict] = [] # 主机配置 self.groups: List[str] = [] # 分组名称列表(保持顺序) self.connections: Dict[str, SSHConnection] = {} # host_id -> SSHConnection self._load_hosts() def _load_hosts(self): if CONFIG_FILE.exists(): try: with open(CONFIG_FILE, "r", encoding="utf-8") as f: data = json.load(f) if isinstance(data, list): # 旧格式:flat list,自动迁移 self.hosts = data self.groups = [] elif isinstance(data, dict): self.hosts = data.get("hosts", []) self.groups = data.get("groups", []) except Exception: self.hosts = [] self.groups = [] if not self.hosts: self.hosts = [{ "id": "demo", "name": "示例主机", "host": "127.0.0.1", "port": 22, "username": "root", "password": "", "key_path": "", "group": "", }] # 给所有旧主机补 group 字段 for h in self.hosts: if "group" not in h: h["group"] = "" def save_hosts(self): data = {"groups": self.groups, "hosts": self.hosts} with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def list_hosts(self) -> List[dict]: return list(self.hosts) def list_groups(self) -> List[str]: """返回分组名列表(不含「未分组」)""" return list(self.groups) def add_group(self, name: str) -> bool: """新建分组;返回是否成功""" name = name.strip() if not name or name in self.groups: return False self.groups.append(name) self.save_hosts() return True def remove_group(self, name: str): """删除分组;组内主机移到未分组""" if name in self.groups: self.groups.remove(name) for h in self.hosts: if h.get("group") == name: h["group"] = "" self.save_hosts() def rename_group(self, old_name: str, new_name: str) -> bool: if old_name not in self.groups: return False new_name = new_name.strip() if not new_name or new_name in self.groups: return False idx = self.groups.index(old_name) self.groups[idx] = new_name for h in self.hosts: if h.get("group") == old_name: h["group"] = new_name self.save_hosts() return True def move_host_to_group(self, host_id: str, group_name: str): """移动主机到指定分组""" for h in self.hosts: if h.get("id") == host_id: h["group"] = group_name self.save_hosts() return def list_hosts_by_group(self) -> dict: """返回 {group_name: [hosts]},"" 为未分组""" result = {g: [] for g in self.groups} result[""] = [] # 未分组 for h in self.hosts: g = h.get("group", "") if g not in result: result[g] = [] result[g].append(dict(h)) return result def get_host(self, host_id: str) -> Optional[dict]: for h in self.hosts: if h.get("id") == host_id: return dict(h) return None def add_host(self, host_info: dict) -> str: """新增主机;返回 id""" with self._lock: new_id = host_info.get("id") or f"host-{int(__import__('time').time()*1000)}" host_info["id"] = new_id self.hosts.append(host_info) self.save_hosts() return new_id def update_host(self, host_id: str, host_info: dict): with self._lock: for i, h in enumerate(self.hosts): if h.get("id") == host_id: host_info["id"] = host_id self.hosts[i] = host_info self.save_hosts() # 断开旧连接 if host_id in self.connections: self.connections[host_id].disconnect() del self.connections[host_id] return def remove_host(self, host_id: str): with self._lock: self.hosts = [h for h in self.hosts if h.get("id") != host_id] if host_id in self.connections: self.connections[host_id].disconnect() del self.connections[host_id] self.save_hosts() def connect(self, host_id: str) -> tuple: """连接指定主机;返回 (conn, 成功, 消息)""" info = self.get_host(host_id) if not info: return None, False, "主机不存在" with self._lock: conn = self.connections.get(host_id) if conn and conn.connected: return conn, True, "已连接" conn = SSHConnection( host=info["host"], port=info.get("port", 22), username=info.get("username", ""), password=info.get("password", ""), key_path=info.get("key_path", ""), ) ok, msg = conn.connect() if ok: self.connections[host_id] = conn return conn, ok, msg def disconnect(self, host_id: str): with self._lock: if host_id in self.connections: self.connections[host_id].disconnect() del self.connections[host_id] def get_connection(self, host_id: str) -> Optional[SSHConnection]: return self.connections.get(host_id) def close_all(self): with self._lock: for c in self.connections.values(): c.disconnect() self.connections.clear() def load_ai_config() -> dict: if AI_CONFIG_FILE.exists(): try: with open(AI_CONFIG_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return { "api_key": "", "base_url": "https://api.openai.com/v1", "model": "gpt-4o-mini", "system_prompt": "", } def save_ai_config(cfg: dict): with open(AI_CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(cfg, f, ensure_ascii=False, indent=2)