diff --git a/client/windows-client.py b/client/windows-client.py new file mode 100644 index 0000000..067ca0d --- /dev/null +++ b/client/windows-client.py @@ -0,0 +1,568 @@ +import sys +import json +import requests +from PySide6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, + QGroupBox, QLabel, QLineEdit, QPushButton, QTextEdit, QTableWidget, + QTableWidgetItem, QComboBox, QSpinBox, QFileDialog, QMessageBox, + QStatusBar, QHeaderView, QFormLayout, QCheckBox +) +from PySide6.QtCore import Qt, QTimer, QThread, Signal +from PySide6.QtGui import QIcon + +# API通信类 +class ApiClient: + def __init__(self): + self.base_url = "http://127.0.0.1:8000/api" + self.token = "" + + def set_server(self, base_url): + self.base_url = base_url.rstrip('/') + + def set_token(self, token): + self.token = token + + def _get_headers(self): + return { + "Authorization": f"Token {self.token}", + "Content-Type": "application/json" + } + + def get_tasks(self): + try: + response = requests.get(f"{self.base_url}/tasks/", headers=self._get_headers()) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"获取任务失败: {str(e)}") + + def create_task(self, name, client_name, script, timeout_seconds): + try: + data = { + "name": name, + "client_name": client_name or None, + "script": script or None, + "timeout_seconds": timeout_seconds + } + response = requests.post(f"{self.base_url}/tasks/", headers=self._get_headers(), json=data) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"创建任务失败: {str(e)}") + + def claim_task(self, client_name): + try: + data = {"client_name": client_name} + response = requests.post(f"{self.base_url}/tasks/claim/", headers=self._get_headers(), json=data) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"认领任务失败: {str(e)}") + + def start_task(self, task_id): + try: + response = requests.post(f"{self.base_url}/tasks/{task_id}/start/", headers=self._get_headers()) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"开始任务失败: {str(e)}") + + def complete_task(self, task_id, status, message): + try: + data = { + "status": status, + "message": message + } + response = requests.post(f"{self.base_url}/tasks/{task_id}/complete/", headers=self._get_headers(), json=data) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"完成任务失败: {str(e)}") + + def upload_result(self, task_id, client_id, status, message, result_file=None): + try: + headers = { + "Authorization": f"Token {self.token}" + } + data = { + "task": task_id, + "client": client_id, + "status": status, + "message": message + } + files = {} + if result_file: + files["result_file"] = open(result_file, "rb") + + response = requests.post(f"{self.base_url}/task_results/", headers=headers, data=data, files=files) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"上传结果失败: {str(e)}") + +# 任务列表更新线程 +class TaskUpdateThread(QThread): + update_signal = Signal(list) + error_signal = Signal(str) + + def __init__(self, api_client): + super().__init__() + self.api_client = api_client + self.running = True + + def run(self): + while self.running: + try: + tasks = self.api_client.get_tasks() + self.update_signal.emit(tasks) + except Exception as e: + self.error_signal.emit(str(e)) + self.sleep(5) # 每5秒更新一次 + + def stop(self): + self.running = False + self.wait() + +# 主应用窗口 +class TaskCenterClient(QMainWindow): + def __init__(self): + super().__init__() + self.api_client = ApiClient() + self.task_update_thread = None + self.init_ui() + + def init_ui(self): + self.setWindowTitle("任务中心客户端") + self.setGeometry(100, 100, 1000, 700) + + # 创建中心部件 + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # 主布局 + main_layout = QVBoxLayout(central_widget) + + # 创建标签页 + self.tabs = QTabWidget() + main_layout.addWidget(self.tabs) + + # 添加配置标签页 + self.create_config_tab() + + # 添加任务列表标签页 + self.create_task_list_tab() + + # 添加任务创建标签页 + self.create_task_create_tab() + + # 添加任务操作标签页 + self.create_task_operation_tab() + + # 创建状态栏 + self.status_bar = QStatusBar() + self.setStatusBar(self.status_bar) + self.status_bar.showMessage("已连接到服务器") + + def create_config_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # 服务器配置 + server_group = QGroupBox("服务器配置") + server_layout = QFormLayout() + + self.server_url_edit = QLineEdit("http://127.0.0.1:8000/api") + server_layout.addRow(QLabel("服务器地址:"), self.server_url_edit) + + self.token_edit = QLineEdit() + self.token_edit.setEchoMode(QLineEdit.Password) + server_layout.addRow(QLabel("API Token:"), self.token_edit) + + # 显示/隐藏Token按钮 + self.token_visibility_btn = QPushButton("显示") + self.token_visibility_btn.clicked.connect(self.toggle_token_visibility) + server_layout.addRow(QLabel(" "), self.token_visibility_btn) + + self.save_config_btn = QPushButton("保存配置") + self.save_config_btn.clicked.connect(self.save_config) + server_layout.addRow(QLabel(" "), self.save_config_btn) + + server_group.setLayout(server_layout) + layout.addWidget(server_group) + + # 测试连接按钮 + self.test_connection_btn = QPushButton("测试连接") + self.test_connection_btn.clicked.connect(self.test_connection) + layout.addWidget(self.test_connection_btn) + + # 连接状态 + self.connection_status = QLabel("未连接") + self.connection_status.setStyleSheet("color: red;") + layout.addWidget(self.connection_status, alignment=Qt.AlignCenter) + + self.tabs.addTab(tab, "配置") + + def create_task_list_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # 刷新按钮 + refresh_layout = QHBoxLayout() + self.refresh_tasks_btn = QPushButton("刷新任务列表") + self.refresh_tasks_btn.clicked.connect(self.refresh_tasks) + refresh_layout.addWidget(self.refresh_tasks_btn) + + self.auto_refresh_checkbox = QCheckBox("自动刷新") + self.auto_refresh_checkbox.stateChanged.connect(self.toggle_auto_refresh) + refresh_layout.addWidget(self.auto_refresh_checkbox) + + layout.addLayout(refresh_layout) + + # 任务列表 + self.task_table = QTableWidget() + self.task_table.setColumnCount(8) + self.task_table.setHorizontalHeaderLabels([ + "ID", "名称", "指定客户端", "实际客户端", "状态", + "创建时间", "开始时间", "完成时间" + ]) + self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) + self.task_table.horizontalHeader().setStretchLastSection(True) + self.task_table.cellClicked.connect(self.on_task_selected) + layout.addWidget(self.task_table) + + # 任务详情 + self.task_detail = QTextEdit() + self.task_detail.setReadOnly(True) + self.task_detail.setPlaceholderText("点击任务查看详情") + layout.addWidget(QLabel("任务详情:")) + layout.addWidget(self.task_detail) + + self.tabs.addTab(tab, "任务列表") + + def create_task_create_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # 任务创建表单 + form_layout = QFormLayout() + + self.task_name_edit = QLineEdit() + form_layout.addRow(QLabel("任务名称:"), self.task_name_edit) + + self.task_client_edit = QLineEdit() + form_layout.addRow(QLabel("指定客户端:"), self.task_client_edit) + + self.task_script_edit = QTextEdit() + self.task_script_edit.setPlaceholderText("输入执行脚本(可选)") + form_layout.addRow(QLabel("执行脚本:"), self.task_script_edit) + + self.task_timeout_spin = QSpinBox() + self.task_timeout_spin.setMinimum(60) + self.task_timeout_spin.setMaximum(86400) + self.task_timeout_spin.setValue(3600) + form_layout.addRow(QLabel("超时时间(秒):"), self.task_timeout_spin) + + self.create_task_btn = QPushButton("创建任务") + self.create_task_btn.clicked.connect(self.create_task) + form_layout.addRow(QLabel(" "), self.create_task_btn) + + layout.addLayout(form_layout) + + # 创建结果 + self.create_result = QTextEdit() + self.create_result.setReadOnly(True) + self.create_result.setPlaceholderText("创建任务结果将显示在这里") + layout.addWidget(QLabel("创建结果:")) + layout.addWidget(self.create_result) + + self.tabs.addTab(tab, "创建任务") + + def create_task_operation_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # 任务选择 + task_select_layout = QHBoxLayout() + self.selected_task_id = QLineEdit() + self.selected_task_id.setPlaceholderText("输入任务ID") + task_select_layout.addWidget(QLabel("任务ID:")) + task_select_layout.addWidget(self.selected_task_id) + + self.select_from_list_btn = QPushButton("从列表选择") + self.select_from_list_btn.clicked.connect(self.select_task_from_list) + task_select_layout.addWidget(self.select_from_list_btn) + + layout.addLayout(task_select_layout) + + # 操作按钮组 + operations_layout = QHBoxLayout() + + self.claim_task_btn = QPushButton("认领任务") + self.claim_task_btn.clicked.connect(self.claim_task) + operations_layout.addWidget(self.claim_task_btn) + + self.start_task_btn = QPushButton("开始任务") + self.start_task_btn.clicked.connect(self.start_task) + operations_layout.addWidget(self.start_task_btn) + + self.complete_task_btn = QPushButton("完成任务") + self.complete_task_btn.clicked.connect(self.complete_task) + operations_layout.addWidget(self.complete_task_btn) + + layout.addLayout(operations_layout) + + # 完成任务表单 + complete_layout = QFormLayout() + + self.complete_status_combo = QComboBox() + self.complete_status_combo.addItems(["success", "failed"]) + complete_layout.addRow(QLabel("执行状态:"), self.complete_status_combo) + + self.complete_message_edit = QTextEdit() + self.complete_message_edit.setPlaceholderText("输入执行结果消息") + complete_layout.addRow(QLabel("执行消息:"), self.complete_message_edit) + + # 文件上传 + self.result_file_edit = QLineEdit() + self.result_file_edit.setReadOnly(True) + browse_btn = QPushButton("浏览") + browse_btn.clicked.connect(self.browse_result_file) + complete_layout.addRow(QLabel("结果文件:"), self.result_file_edit) + complete_layout.addRow(QLabel(" "), browse_btn) + + layout.addLayout(complete_layout) + + # 操作结果 + self.operation_result = QTextEdit() + self.operation_result.setReadOnly(True) + self.operation_result.setPlaceholderText("操作结果将显示在这里") + layout.addWidget(QLabel("操作结果:")) + layout.addWidget(self.operation_result) + + self.tabs.addTab(tab, "任务操作") + + def toggle_token_visibility(self): + if self.token_edit.echoMode() == QLineEdit.Password: + self.token_edit.setEchoMode(QLineEdit.Normal) + self.token_visibility_btn.setText("隐藏") + else: + self.token_edit.setEchoMode(QLineEdit.Password) + self.token_visibility_btn.setText("显示") + + def save_config(self): + server_url = self.server_url_edit.text() + token = self.token_edit.text() + + if not server_url or not token: + QMessageBox.warning(self, "警告", "服务器地址和API Token不能为空!") + return + + # 保存配置到文件 + try: + config = { + "server_url": server_url, + "token": token + } + with open("client_config.json", "w") as f: + json.dump(config, f) + + QMessageBox.information(self, "成功", "配置已保存!") + self.api_client.set_server(server_url) + self.api_client.set_token(token) + except Exception as e: + QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}") + + def test_connection(self): + server_url = self.server_url_edit.text() + token = self.token_edit.text() + + if not server_url or not token: + QMessageBox.warning(self, "警告", "服务器地址和API Token不能为空!") + return + + try: + # 测试连接 + temp_client = ApiClient() + temp_client.set_server(server_url) + temp_client.set_token(token) + tasks = temp_client.get_tasks() + + QMessageBox.information(self, "成功", f"连接成功!找到 {len(tasks)} 个任务") + self.connection_status.setText("已连接") + self.connection_status.setStyleSheet("color: green;") + except Exception as e: + QMessageBox.critical(self, "错误", f"连接失败: {str(e)}") + self.connection_status.setText(f"连接失败: {str(e)}") + self.connection_status.setStyleSheet("color: red;") + + def refresh_tasks(self): + try: + tasks = self.api_client.get_tasks() + self.update_task_table(tasks) + self.status_bar.showMessage(f"刷新完成,共 {len(tasks)} 个任务") + except Exception as e: + QMessageBox.critical(self, "错误", str(e)) + self.status_bar.showMessage(f"刷新失败: {str(e)}") + + def update_task_table(self, tasks): + self.task_table.setRowCount(len(tasks)) + + for row, task in enumerate(tasks): + self.task_table.setItem(row, 0, QTableWidgetItem(str(task["id"]))) + self.task_table.setItem(row, 1, QTableWidgetItem(task["name"])) + self.task_table.setItem(row, 2, QTableWidgetItem(task["client_name"] or "未指定")) + self.task_table.setItem(row, 3, QTableWidgetItem(task["assigned_to"] or "未分配")) + self.task_table.setItem(row, 4, QTableWidgetItem(task["status"])) + self.task_table.setItem(row, 5, QTableWidgetItem(task["created_at"])) + self.task_table.setItem(row, 6, QTableWidgetItem(task["started_at"] or "-")) + self.task_table.setItem(row, 7, QTableWidgetItem(task["completed_at"] or "-")) + + def toggle_auto_refresh(self, state): + if state == Qt.Checked: + # 启动自动刷新线程 + self.task_update_thread = TaskUpdateThread(self.api_client) + self.task_update_thread.update_signal.connect(self.update_task_table) + self.task_update_thread.error_signal.connect(self.show_thread_error) + self.task_update_thread.start() + self.status_bar.showMessage("已启动自动刷新") + else: + # 停止自动刷新线程 + if self.task_update_thread: + self.task_update_thread.stop() + self.task_update_thread = None + self.status_bar.showMessage("已停止自动刷新") + + def show_thread_error(self, error_msg): + self.status_bar.showMessage(f"自动刷新失败: {error_msg}") + + def on_task_selected(self, row, column): + task_id = self.task_table.item(row, 0).text() + self.selected_task_id.setText(task_id) + + # 显示任务详情 + task_name = self.task_table.item(row, 1).text() + client_name = self.task_table.item(row, 2).text() + assigned_to = self.task_table.item(row, 3).text() + status = self.task_table.item(row, 4).text() + created_at = self.task_table.item(row, 5).text() + started_at = self.task_table.item(row, 6).text() + completed_at = self.task_table.item(row, 7).text() + + detail = f"任务ID: {task_id}\n" + detail += f"任务名称: {task_name}\n" + detail += f"指定客户端: {client_name}\n" + detail += f"实际客户端: {assigned_to}\n" + detail += f"状态: {status}\n" + detail += f"创建时间: {created_at}\n" + detail += f"开始时间: {started_at}\n" + detail += f"完成时间: {completed_at}\n" + + self.task_detail.setPlainText(detail) + + def toggle_token_visibility(self): + if self.token_edit.echoMode() == QLineEdit.Password: + self.token_edit.setEchoMode(QLineEdit.Normal) + self.token_visibility_btn.setText("隐藏") + else: + self.token_edit.setEchoMode(QLineEdit.Password) + self.token_visibility_btn.setText("显示") + + def create_task(self): + name = self.task_name_edit.text() + client_name = self.task_client_edit.text() + script = self.task_script_edit.toPlainText() + timeout = self.task_timeout_spin.value() + + if not name: + QMessageBox.warning(self, "警告", "任务名称不能为空!") + return + + try: + result = self.api_client.create_task(name, client_name, script, timeout) + self.create_result.setPlainText(json.dumps(result, indent=2, ensure_ascii=False)) + QMessageBox.information(self, "成功", "任务创建成功!") + + # 清空表单 + self.task_name_edit.clear() + self.task_client_edit.clear() + self.task_script_edit.clear() + self.task_timeout_spin.setValue(3600) + except Exception as e: + self.create_result.setPlainText(str(e)) + QMessageBox.critical(self, "错误", str(e)) + + def claim_task(self): + task_id = self.selected_task_id.text() + if not task_id: + QMessageBox.warning(self, "警告", "请输入任务ID!") + return + + try: + result = self.api_client.claim_task(self.task_client_edit.text() or "default_client") + self.operation_result.setPlainText(json.dumps(result, indent=2, ensure_ascii=False)) + QMessageBox.information(self, "成功", "任务认领成功!") + except Exception as e: + self.operation_result.setPlainText(str(e)) + QMessageBox.critical(self, "错误", str(e)) + + def start_task(self): + task_id = self.selected_task_id.text() + if not task_id: + QMessageBox.warning(self, "警告", "请输入任务ID!") + return + + try: + result = self.api_client.start_task(task_id) + self.operation_result.setPlainText(json.dumps(result, indent=2, ensure_ascii=False)) + QMessageBox.information(self, "成功", "任务开始成功!") + except Exception as e: + self.operation_result.setPlainText(str(e)) + QMessageBox.critical(self, "错误", str(e)) + + def complete_task(self): + task_id = self.selected_task_id.text() + if not task_id: + QMessageBox.warning(self, "警告", "请输入任务ID!") + return + + status = self.complete_status_combo.currentText() + message = self.complete_message_edit.toPlainText() + + try: + result = self.api_client.complete_task(task_id, status, message) + self.operation_result.setPlainText(json.dumps(result, indent=2, ensure_ascii=False)) + QMessageBox.information(self, "成功", "任务完成成功!") + except Exception as e: + self.operation_result.setPlainText(str(e)) + QMessageBox.critical(self, "错误", str(e)) + + def select_task_from_list(self): + selected_rows = self.task_table.selectedItems() + if selected_rows: + task_id = selected_rows[0].text() + self.selected_task_id.setText(task_id) + + def browse_result_file(self): + file_path, _ = QFileDialog.getOpenFileName(self, "选择结果文件") + if file_path: + self.result_file_edit.setText(file_path) + + def load_config(self): + try: + with open("client_config.json", "r") as f: + config = json.load(f) + self.server_url_edit.setText(config.get("server_url", "http://127.0.0.1:8000/api")) + self.token_edit.setText(config.get("token", "")) + self.api_client.set_server(config.get("server_url", "http://127.0.0.1:8000/api")) + self.api_client.set_token(config.get("token", "")) + except FileNotFoundError: + pass # 配置文件不存在,使用默认值 + except Exception as e: + QMessageBox.warning(self, "警告", f"加载配置失败: {str(e)}") + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = TaskCenterClient() + window.load_config() + window.show() + sys.exit(app.exec()) \ No newline at end of file