a57dbc0252
功能: - 多主机管理 (增删改查, 密码/私钥双认证, 导入导出) - 远程终端 (命令执行 + 常用命令快捷栏 + 超时控制) - SFTP 文件浏览 (上传/下载带进度, 新建/删除/重命名) - 实时监控 (CPU/内存/磁盘/网络, 1-10秒可调刷新) - AI Agent (OpenAI 兼容 API, 5 工具自动调用: 命令/指标/列文件/读文件/上传) 技术栈: PyQt5 + paramiko + psutil + requests + PyInstaller 打包: build_windows.bat / build.sh 一键产出 ~57MB 单文件 exe 测试: core 6/6 + UI 3/3 + E2E 6/6 全部通过
140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
"""
|
|
连接管理器:保存多个 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.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:
|
|
self.hosts = json.load(f)
|
|
except Exception:
|
|
self.hosts = []
|
|
if not self.hosts:
|
|
# 给一个示例条目,让 UI 不为空
|
|
self.hosts = [{
|
|
"id": "demo", "name": "示例主机", "host": "127.0.0.1",
|
|
"port": 22, "username": "root", "password": "", "key_path": "",
|
|
}]
|
|
|
|
def save_hosts(self):
|
|
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
json.dump(self.hosts, f, ensure_ascii=False, indent=2)
|
|
|
|
def list_hosts(self) -> List[dict]:
|
|
return list(self.hosts)
|
|
|
|
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)
|