feat: host groups - tree view with folders, drag-free context menu
Data model:
- hosts.json upgraded to {groups: [...], hosts: [...]}, auto-migrates
old flat-list format on load
- each host gets group field (default '' = ungrouped)
Manager API (core/manager.py):
- add_group / remove_group / rename_group / move_host_to_group
- list_groups / list_hosts_by_group
UI (ui/main_window.py):
- QListWidget replaced with QTreeWidget (folder tree)
- Group nodes: 📁 name (count), expandable/collapsible
- Ungrouped always shown as 📭 未分组
- Right-click context menu:
- Empty area / group: New group, Rename, Delete
- Host: Connect, Edit, Delete, Move to group (submenu)
- Double-click host = connect, double-click group = expand/collapse
- All 6 test suites pass
This commit is contained in:
+73
-4
@@ -17,12 +17,13 @@ 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()
|
||||
|
||||
@@ -30,23 +31,91 @@ class ConnectionManager:
|
||||
if CONFIG_FILE.exists():
|
||||
try:
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||
self.hosts = json.load(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:
|
||||
# 给一个示例条目,让 UI 不为空
|
||||
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(self.hosts, f, ensure_ascii=False, indent=2)
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user