""" 后台工作线程:把阻塞操作(连接、执行命令、上传、AI 推理)从 UI 线程中剥离。 所有线程通过信号与 UI 通信,UI 不阻塞。 """ import time import traceback from typing import Optional from PyQt5.QtCore import QThread, pyqtSignal from core.ssh_client import SSHConnection from core.monitor import SystemMonitor from core.ai_agent import AIAgent from core.manager import ConnectionManager class ConnectWorker(QThread): """后台建立 SSH 连接""" finished_with = pyqtSignal(str, bool, str) # host_id, ok, msg def __init__(self, manager: ConnectionManager, host_id: str): super().__init__() self.manager = manager self.host_id = host_id def run(self): conn, ok, msg = self.manager.connect(self.host_id) self.finished_with.emit(self.host_id, ok, msg) class CommandWorker(QThread): """后台执行远程命令""" finished_with = pyqtSignal(int, str, str) # exit_code, stdout, stderr def __init__(self, conn: SSHConnection, command: str, timeout: int = 30): super().__init__() self.conn = conn self.command = command self.timeout = timeout def run(self): try: code, out, err = self.conn.exec_command(self.command, timeout=self.timeout) except Exception as e: code, out, err = -1, "", str(e) self.finished_with.emit(code, out, err) class MonitorWorker(QThread): """后台采集系统指标;循环模式""" sample_ready = pyqtSignal(dict) error = pyqtSignal(str) def __init__(self, conn: SSHConnection, interval: int = 3): super().__init__() self.conn = conn self.interval = interval self._stop = False def stop(self): self._stop = True def run(self): while not self._stop: try: m = SystemMonitor.collect(self.conn) self.sample_ready.emit(m) except Exception as e: self.error.emit(str(e)) for _ in range(self.interval * 10): if self._stop: return time.sleep(0.1) class UploadWorker(QThread): """后台 SFTP 上传(带进度)""" progress = pyqtSignal(int, int) # done, total finished_with = pyqtSignal(bool, str) def __init__(self, conn: SSHConnection, local: str, remote: str): super().__init__() self.conn = conn self.local = local self.remote = remote def run(self): try: ok, msg = self.conn.upload(self.local, self.remote, progress_cb=lambda d, t: self.progress.emit(d, t)) except Exception as e: ok, msg = False, f"异常: {e}" self.finished_with.emit(ok, msg) class DownloadWorker(QThread): """后台 SFTP 下载(带进度)""" progress = pyqtSignal(int, int) finished_with = pyqtSignal(bool, str) def __init__(self, conn: SSHConnection, remote: str, local: str): super().__init__() self.conn = conn self.remote = remote self.local = local def run(self): try: ok, msg = self.conn.download(self.remote, self.local, progress_cb=lambda d, t: self.progress.emit(d, t)) except Exception as e: ok, msg = False, f"异常: {e}" self.finished_with.emit(ok, msg) class ListDirWorker(QThread): """后台列目录""" finished_with = pyqtSignal(str, list) # path, entries def __init__(self, conn: SSHConnection, path: str): super().__init__() self.conn = conn self.path = path def run(self): entries = self.conn.list_dir(self.path) self.finished_with.emit(self.path, entries) class AIWorker(QThread): """后台 AI 对话(避免 UI 卡顿)""" step = pyqtSignal(str, str) # role, content finished_with = pyqtSignal(str) # final reply error = pyqtSignal(str) def __init__(self, agent: AIAgent, user_msg: str, conn: Optional[SSHConnection]): super().__init__() self.agent = agent self.user_msg = user_msg self.conn = conn def _tool(self, name: str, args: dict) -> str: """AI 工具调用 -> 真实执行""" try: if name == "exec_ssh_command": if not self.conn or not self.conn.connected: return "[错误] 当前没有可用的 SSH 连接" cmd = args.get("command", "") timeout = min(int(args.get("timeout", 30)), 300) code, out, err = self.conn.exec_command(cmd, timeout=timeout) out_s = out[:3000] + ("\n...(stdout 截断)" if len(out) > 3000 else "") err_s = err[:1500] + ("\n...(stderr 截断)" if len(err) > 1500 else "") return f"exit={code}\nstdout:\n{out_s}\nstderr:\n{err_s}" if name == "get_system_metrics": m = SystemMonitor.collect(self.conn) if self.conn else {} # 简化展示给模型 summary = ( f"hostname={m.get('hostname','')} os={m.get('os','')} " f"cpu={m.get('cpu',0):.1f}% cores={m.get('cores',1)} " f"load1={m.get('load1',0):.2f} " f"mem={m.get('mem_percent',0):.1f}% " f"({SystemMonitor.format_bytes(m.get('mem_used',0))}/" f"{SystemMonitor.format_bytes(m.get('mem_total',0))})" ) disks = "; ".join( f"{d['mount']}={d['percent']}%" for d in m.get("disks", []) ) nets = "; ".join( f"{n['iface']}(rx={SystemMonitor.format_bytes(n['rx'])}," f"tx={SystemMonitor.format_bytes(n['tx'])})" for n in m.get("net", []) ) return f"{summary}\ndisks: {disks}\nnet: {nets}" if name == "list_remote_files": if not self.conn or not self.conn.connected: return "[错误] 当前没有可用的 SSH 连接" path = args.get("path", "/") entries = self.conn.list_dir(path) if not entries: return f"(空目录或无权限: {path})" lines = [] for e in entries[:200]: tag = "d" if e["is_dir"] else "-" lines.append(f"{tag} {e['size']:>10} {e['name']}") return f"path={path}\n" + "\n".join(lines) if name == "read_remote_file": if not self.conn or not self.conn.connected: return "[错误] 当前没有可用的 SSH 连接" path = args.get("path", "") if not path: return "[错误] 缺少 path 参数" # 用 cat,避免 SFTP 打开大文件 code, out, err = self.conn.exec_command(f"cat '{path}' 2>&1 | head -c 8000") return out if code == 0 else f"[错误 exit={code}] {err}" if name == "upload_local_file": if not self.conn or not self.conn.connected: return "[错误] 当前没有可用的 SSH 连接" local = args.get("local_path", "") remote = args.get("remote_path", "") if not local or not remote: return "[错误] 缺少 local_path 或 remote_path" import os if not os.path.isfile(local): return f"[错误] 本地文件不存在: {local}" ok, msg = self.conn.upload(local, remote) return ("成功: " if ok else "失败: ") + msg return f"[未知工具] {name}" except Exception as e: return f"[工具异常 {name}] {e}\n{traceback.format_exc()}" def run(self): try: final = self.agent.chat(self.user_msg, self._tool, on_step=lambda r, c: self.step.emit(r, c)) self.finished_with.emit(final) except Exception as e: self.error.emit(f"{e}\n{traceback.format_exc()}")