PUGE 1 kuukausi sitten
vanhempi
commit
41cbea4b27
9 muutettua tiedostoa jossa 2970 lisäystä ja 366 poistoa
  1. 4 0
      config.json
  2. 0 0
      cookie_config.txt
  3. BIN
      gbild32.dll
  4. BIN
      gbild64.dll
  5. 258 0
      word文本替换工具.py
  6. 1077 177
      三一平台.py
  7. 1063 0
      控制vm虚拟机.py
  8. 0 38
      米画师.spec
  9. 568 151
      飞书提交数据.py

+ 4 - 0
config.json

@@ -0,0 +1,4 @@
+{
+    "username": "深金川",
+    "folder_path": "C:/Users/mail/Documents/GitHub/pythonTest"
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
cookie_config.txt


BIN
gbild32.dll


BIN
gbild64.dll


+ 258 - 0
word文本替换工具.py

@@ -0,0 +1,258 @@
+import tkinter as tk
+from tkinter import ttk, filedialog, messagebox
+import os
+from docx import Document
+import threading
+
+
+class WordReplacerApp:
+    def __init__(self, root):
+        self.root = root
+        self.root.title("Word日期替换工具")
+        self.root.geometry("600x560")
+        self.root.resizable(False, False)
+        
+        # 设置样式
+        style = ttk.Style()
+        style.theme_use('clam')
+        
+        # 创建主框架
+        main_frame = ttk.Frame(root, padding="20")
+        main_frame.pack(fill=tk.BOTH, expand=True)
+        
+        # 标题
+        title_label = ttk.Label(main_frame, text="Word文档日期批量替换工具", 
+                                font=("微软雅黑", 16, "bold"))
+        title_label.pack(pady=(0, 20))
+        
+        # 说明区域
+        info_frame = ttk.LabelFrame(main_frame, text="替换规则", padding="10")
+        info_frame.pack(fill=tk.X, pady=(0, 15))
+        
+        rule1 = ttk.Label(info_frame, text="• 将 \"xx月\" 替换为 \"07月\"")
+        rule1.pack(anchor=tk.W, pady=2)
+        rule2 = ttk.Label(info_frame, text="• 将 \"xx日\" 替换为 \"08日\"")
+        rule2.pack(anchor=tk.W, pady=2)
+        rule3 = ttk.Label(info_frame, text="• 支持同时替换月和日", foreground="gray")
+        rule3.pack(anchor=tk.W, pady=2)
+        
+        # 文件选择区域
+        file_frame = ttk.LabelFrame(main_frame, text="文件选择", padding="10")
+        file_frame.pack(fill=tk.X, pady=(0, 15))
+        
+        # 文件路径显示
+        self.file_path_var = tk.StringVar()
+        self.file_path_var.set("未选择文件")
+        
+        path_entry = ttk.Entry(file_frame, textvariable=self.file_path_var, 
+                               state='readonly', font=("微软雅黑", 10))
+        path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
+        
+        select_btn = ttk.Button(file_frame, text="选择Word文件", 
+                                command=self.select_file, width=12)
+        select_btn.pack(side=tk.RIGHT)
+        
+        # 选项区域
+        options_frame = ttk.LabelFrame(main_frame, text="替换选项", padding="10")
+        options_frame.pack(fill=tk.X, pady=(0, 15))
+        
+        # 替换月份选项
+        self.replace_month_var = tk.BooleanVar(value=True)
+        month_check = ttk.Checkbutton(options_frame, text="替换月份 (xx月 → 07月)", 
+                                      variable=self.replace_month_var)
+        month_check.pack(anchor=tk.W, pady=2)
+        
+        # 替换日期选项
+        self.replace_day_var = tk.BooleanVar(value=True)
+        day_check = ttk.Checkbutton(options_frame, text="替换日期 (xx日 → 08日)", 
+                                    variable=self.replace_day_var)
+        day_check.pack(anchor=tk.W, pady=2)
+        
+        # 进度条
+        self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
+        self.progress.pack(fill=tk.X, pady=(0, 15))
+        
+        # 操作按钮区域
+        btn_frame = ttk.Frame(main_frame)
+        btn_frame.pack(fill=tk.X, pady=(0, 10))
+        
+        self.process_btn = ttk.Button(btn_frame, text="开始替换", 
+                                      command=self.start_process, width=15)
+        self.process_btn.pack(side=tk.LEFT, padx=(0, 10))
+        
+        self.save_btn = ttk.Button(btn_frame, text="另存为...", 
+                                   command=self.save_as, width=15, state='disabled')
+        self.save_btn.pack(side=tk.LEFT)
+        
+        # 状态栏
+        self.status_var = tk.StringVar()
+        self.status_var.set("就绪 - 请选择Word文件")
+        status_label = ttk.Label(main_frame, textvariable=self.status_var, 
+                                 relief=tk.SUNKEN, anchor=tk.W, padding=(5, 2))
+        status_label.pack(fill=tk.X, pady=(10, 0))
+        
+        # 存储处理后的文档
+        self.processed_doc = None
+        self.original_path = None
+        self.output_path = None
+        
+    def select_file(self):
+        """选择Word文件"""
+        file_path = filedialog.askopenfilename(
+            title="选择Word文档",
+            filetypes=[("Word文档", "*.docx"), ("所有文件", "*.*")]
+        )
+        
+        if file_path:
+            self.file_path_var.set(file_path)
+            self.original_path = file_path
+            self.processed_doc = None
+            self.output_path = None
+            self.save_btn.config(state='disabled')
+            self.status_var.set(f"已选择: {os.path.basename(file_path)}")
+    
+    def start_process(self):
+        """开始处理文档"""
+        if not self.original_path:
+            messagebox.showwarning("警告", "请先选择一个Word文件!")
+            return
+        
+        # 检查文件是否存在
+        if not os.path.exists(self.original_path):
+            messagebox.showerror("错误", "文件不存在,请重新选择!")
+            return
+        
+        # 检查是否至少选择了一个替换选项
+        if not self.replace_month_var.get() and not self.replace_day_var.get():
+            messagebox.showwarning("警告", "请至少选择一种替换选项!")
+            return
+        
+        # 禁用按钮
+        self.process_btn.config(state='disabled')
+        self.save_btn.config(state='disabled')
+        self.progress.start(10)
+        self.status_var.set("正在处理文档...")
+        
+        # 在新线程中处理
+        thread = threading.Thread(target=self.process_document)
+        thread.daemon = True
+        thread.start()
+    
+    def process_document(self):
+        """处理文档的核心逻辑"""
+        try:
+            # 加载文档
+            doc = Document(self.original_path)
+            replace_count = 0
+            
+            # 遍历所有段落
+            for paragraph in doc.paragraphs:
+                text = paragraph.text
+                modified = False
+                
+                # 替换月份
+                if self.replace_month_var.get() and "xx月" in text:
+                    text = text.replace("xx月", "07月")
+                    modified = True
+                    replace_count += 1
+                
+                # 替换日期
+                if self.replace_day_var.get() and "xx日" in text:
+                    text = text.replace("xx日", "08日")
+                    modified = True
+                    replace_count += 1
+                
+                # 如果段落被修改,更新内容
+                if modified:
+                    # 保留原有格式
+                    paragraph.clear()
+                    paragraph.add_run(text)
+            
+            # 遍历所有表格中的单元格
+            for table in doc.tables:
+                for row in table.rows:
+                    for cell in row.cells:
+                        for paragraph in cell.paragraphs:
+                            text = paragraph.text
+                            modified = False
+                            
+                            if self.replace_month_var.get() and "xx月" in text:
+                                text = text.replace("xx月", "07月")
+                                modified = True
+                                replace_count += 1
+                            
+                            if self.replace_day_var.get() and "xx日" in text:
+                                text = text.replace("xx日", "08日")
+                                modified = True
+                                replace_count += 1
+                            
+                            if modified:
+                                paragraph.clear()
+                                paragraph.add_run(text)
+            
+            # 保存处理后的文档
+            self.processed_doc = doc
+            self.replace_count = replace_count
+            
+            # 更新UI
+            self.root.after(0, self.process_complete, replace_count)
+            
+        except Exception as e:
+            self.root.after(0, self.process_error, str(e))
+    
+    def process_complete(self, count):
+        """处理完成回调"""
+        self.progress.stop()
+        self.process_btn.config(state='normal')
+        self.save_btn.config(state='normal')
+        
+        if count > 0:
+            self.status_var.set(f"处理完成!共替换 {count} 处")
+            messagebox.showinfo("成功", f"文档处理完成!\n共进行了 {count} 处替换。\n请点击'另存为...'保存文件。")
+        else:
+            self.status_var.set("处理完成!未找到需要替换的内容")
+            messagebox.showinfo("提示", "未找到需要替换的 'xx月' 或 'xx日' 内容。")
+    
+    def process_error(self, error_msg):
+        """处理错误回调"""
+        self.progress.stop()
+        self.process_btn.config(state='normal')
+        self.save_btn.config(state='disabled')
+        self.status_var.set(f"错误: {error_msg}")
+        messagebox.showerror("错误", f"处理文档时出错:\n{error_msg}")
+    
+    def save_as(self):
+        """另存为功能"""
+        if not self.processed_doc:
+            messagebox.showwarning("警告", "请先处理文档!")
+            return
+        
+        # 生成默认文件名
+        base_name = os.path.splitext(os.path.basename(self.original_path))[0]
+        default_name = f"{base_name}_替换后.docx"
+        
+        save_path = filedialog.asksaveasfilename(
+            title="保存文档",
+            defaultextension=".docx",
+            filetypes=[("Word文档", "*.docx"), ("所有文件", "*.*")],
+            initialfile=default_name
+        )
+        
+        if save_path:
+            try:
+                self.processed_doc.save(save_path)
+                self.output_path = save_path
+                self.status_var.set(f"已保存: {os.path.basename(save_path)}")
+                messagebox.showinfo("成功", f"文档已保存到:\n{save_path}")
+            except Exception as e:
+                messagebox.showerror("错误", f"保存文件时出错:\n{e}")
+
+
+def main():
+    root = tk.Tk()
+    app = WordReplacerApp(root)
+    root.mainloop()
+
+
+if __name__ == "__main__":
+    main()

+ 1077 - 177
三一平台.py

@@ -3,17 +3,211 @@ import hashlib
 import requests
 import json
 import time
+import threading
+import os
+import re
+import ssl
+import subprocess
 from datetime import datetime, timedelta
 from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                              QHBoxLayout, QLabel, QLineEdit, QPushButton, 
                              QTableWidget, QTableWidgetItem, QHeaderView, 
                              QCheckBox, QMessageBox, QGroupBox, QTextEdit,
-                             QGridLayout, QSplitter)
+                             QGridLayout, QSplitter, QComboBox, QFileDialog)
 from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
-from PyQt5.QtGui import QTextDocument
+import websocket
+import threading
+import time
+
+# ==================== WebSocket 客户端 ====================
+
+class WebSocketClient(QThread):
+    connected = pyqtSignal()
+    disconnected = pyqtSignal()
+    message_received = pyqtSignal(dict)
+    error = pyqtSignal(str)
+    
+    def __init__(self, get_folder_callback=None):
+        super().__init__()
+        self.ws = None
+        self.is_running = False
+        self.user_id = None
+        self.type_str = "自动扫码"
+        self.is_admin = True
+        self.device_id = None
+        self.ws_lock = threading.Lock()
+        self.get_folder_callback = get_folder_callback  # 获取文件夹路径的回调函数
+        
+    def connect(self, device_id):
+        """连接WebSocket服务器"""
+        self.device_id = device_id
+        self.is_running = True
+        self.start()
+    
+    def run(self):
+        """在子线程中运行WebSocket连接"""
+        try:
+            self._connect_ws()
+        except Exception as e:
+            self.error.emit(f"WebSocket连接错误: {str(e)}")
+    
+    def _connect_ws(self):
+        """建立WebSocket连接并处理消息"""
+        while self.is_running:
+            try:
+                # 创建WebSocketApp时添加ssl_options参数禁用证书验证
+                self.ws = websocket.WebSocketApp(
+                    'wss://ws.lamp.run',
+                    on_open=self._on_open,
+                    on_message=self._on_message,
+                    on_error=self._on_error,
+                    on_close=self._on_close
+                )
+                
+                # 运行WebSocket,禁用SSL证书验证
+                # 添加 sslopt={"cert_reqs": ssl.CERT_NONE} 参数
+                import ssl
+                self.ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
+                
+                # 如果连接断开,等待3秒重连
+                if self.is_running:
+                    time.sleep(3)
+                    
+            except Exception as e:
+                self.error.emit(f"连接异常: {str(e)}")
+                if self.is_running:
+                    time.sleep(3)
+    
+    def _on_open(self, ws):
+        """连接打开时的回调"""
+        self.connected.emit()
+        # 发送登录消息
+        login_msg = {
+            "route": "login",
+            "type": self.type_str,
+            "admin": self.is_admin,
+            "id": self.device_id
+        }
+        ws.send(json.dumps(login_msg))
+    
+    def _on_message(self, ws, message):
+        """接收消息的回调"""
+        try:
+            data = json.loads(message)
+            
+            # 处理用户信息
+            if data.get('type') == 'userInfo':
+                self.user_id = data.get('value', {}).get('userID')
+                if self.user_id:
+                    self.message_received.emit(data)
+            # 处理图片接收
+            elif data.get('type') == 'imgIn':
+                self.handle_image_received(data)
+            else:
+                self.message_received.emit(data)
+                
+        except json.JSONDecodeError:
+            pass
+        except Exception as e:
+            print(f"处理消息异常: {e}")
+
+    def handle_image_received(self, data):
+        """处理接收到的图片"""
+        try:
+            import base64
+            
+            # 获取图片的base64编码
+            img_base64 = data.get('value', '')
+            if not img_base64:
+                self.error.emit("图片数据为空")
+                return
+            
+            # 获取发送者ID
+            sender_id = data.get('id', 'unknown')
+            
+            # 去除可能的base64前缀
+            if ',' in img_base64:
+                img_base64 = img_base64.split(',')[1]
+            
+            # 解码base64
+            image_data = base64.b64decode(img_base64)
+            
+            # 固定保存到C盘根目录
+            filepath = "C:/imgIn.jpeg"
+            
+            # 保存图片
+            with open(filepath, 'wb') as f:
+                f.write(image_data)
+            
+            print(f"✅ 图片已保存: {filepath}")
+            
+            # 通知主窗口图片已保存
+            success_msg = {
+                'type': 'imgSaved',
+                'filepath': filepath,
+                'filename': "imgIn.jpeg",
+                'sender_id': sender_id
+            }
+            self.message_received.emit(success_msg)
+            
+        except Exception as e:
+            error_msg = f"保存图片失败: {str(e)}"
+            print(f"❌ {error_msg}")
+            self.error.emit(error_msg)
+
+    def get_folder_path(self):
+        """获取保存文件夹路径"""
+        if self.get_folder_callback:
+            return self.get_folder_callback()
+        return ""
+    
+    def _on_error(self, ws, error):
+        """错误回调"""
+        self.error.emit(f"WebSocket错误: {str(error)}")
+    
+    def _on_close(self, ws, close_status_code, close_msg):
+        """连接关闭回调"""
+        self.disconnected.emit()
+    
+    def send(self, route, value="", send_to=None):
+        """发送消息,send_to 使用 userID"""
+        with self.ws_lock:
+            if not self.ws:
+                return False
+            
+            try:
+                # 检查连接状态
+                if self.ws.sock and self.ws.sock.connected:
+                    msg = {
+                        "route": route,
+                        "type": self.type_str,
+                        "userID": self.user_id,
+                        "id": send_to,  # send_to 是 userID
+                        "value": value
+                    }
+                    self.ws.send(json.dumps(msg))
+                    return True
+                else:
+                    return False
+            except Exception as e:
+                print(f"发送消息错误: {e}")
+                return False
+    
+    def get_list(self):
+        """获取在线客户端列表"""
+        return self.send("getList")
+    
+    def stop(self):
+        """停止连接"""
+        self.is_running = False
+        if self.ws:
+            try:
+                self.ws.close()
+            except:
+                pass
+# ==================== 登录线程 ====================
 
 class LoginThread(QThread):
-    """登录线程"""
     finished = pyqtSignal(dict)
     error = pyqtSignal(str)
     
@@ -26,10 +220,8 @@ class LoginThread(QThread):
         try:
             md5_password = hashlib.md5(self.password.encode('utf-8')).hexdigest()
             url = f"http://119.91.58.175:9999/gps-web/api/login.jsp?userId={self.username}&password={md5_password}&loginType=user&onlyValidate=false&plateColor=&loginWay=interface&loginLang=zh_CN"
-            
             response = requests.get(url, timeout=10)
             response.encoding = 'utf-8'
-            
             if response.status_code == 200:
                 data = response.json()
                 if data.get('rspCode') == '1':
@@ -41,8 +233,9 @@ class LoginThread(QThread):
         except Exception as e:
             self.error.emit(f"登录异常: {str(e)}")
 
+# ==================== 查询车辆线程 ====================
+
 class QueryThread(QThread):
-    """查询车辆定位线程"""
     finished = pyqtSignal(dict)
     error = pyqtSignal(str)
     
@@ -53,10 +246,8 @@ class QueryThread(QThread):
     def run(self):
         try:
             url = f"http://119.91.58.175:9999/gps-web/api/get_gps_r.jsp?sessionId={self.session_id}&teamId=&carIds=&simple=false"
-            
             response = requests.get(url, timeout=15)
             response.encoding = 'utf-8'
-            
             if response.status_code == 200:
                 data = response.json()
                 if data.get('rspCode') == '1':
@@ -68,158 +259,541 @@ class QueryThread(QThread):
         except Exception as e:
             self.error.emit(f"查询异常: {str(e)}")
 
+# ==================== 查询区域线程 ====================
+
+class RegionQueryThread(QThread):
+    finished = pyqtSignal(dict)
+    error = pyqtSignal(str)
+    
+    def __init__(self, session_id):
+        super().__init__()
+        self.session_id = session_id
+    
+    def run(self):
+        try:
+            url = f"http://119.91.58.175:9999/gps-web/api/get_region_car.jsp?sessionId={self.session_id}&regionIds="
+            response = requests.get(url, timeout=10)
+            response.encoding = 'utf-8'
+            if response.status_code == 200:
+                data = response.json()
+                if data.get('rspCode') == '1':
+                    self.finished.emit(data)
+                else:
+                    self.error.emit(f"查询区域失败: {data.get('msg', '未知错误')}")
+            else:
+                self.error.emit(f"请求失败: HTTP {response.status_code}")
+        except Exception as e:
+            self.error.emit(f"查询异常: {str(e)}")
+
+# ==================== 主窗口 ====================
+
 class MainWindow(QMainWindow):
     def __init__(self):
         super().__init__()
         self.session_id = None
         self.username = None
         self.car_data = []
-        self.car_checked_state = {}  # 存储每辆车的勾选状态
-        self.triggered_records = {}  # 存储触发记录 {car_plate: {'load': last_time, 'unload': last_time}}
+        self.car_checked_state = {}  # 启用状态
+        self.car_scan_state = {}     # 扫码状态(新增)
+        self.triggered_records = {}
         self.is_monitoring = False
-        self.refresh_timer = QTimer()
-        self.refresh_timer.timeout.connect(self.auto_refresh)
+
+        # 扫码任务相关
+        self.scan_task_queue = []  # 待处理的车辆队列
+        self.is_processing_scan = False  # 是否正在处理扫码任务
+        self.scan_timer = QTimer()  # 定时器用于间隔执行
+        self.scan_timer.timeout.connect(self.process_next_scan)
+        
+        # WebSocket相关
+        self.ws_client = None
+        self.online_clients = {}  # userID -> 客户端信息
+        
+        # 文件夹路径
+        self.folder_path = ""
+        
+        # 定时器
+        self.refresh_timer = QTimer()  # 10秒刷新车辆列表
+        self.refresh_timer.timeout.connect(self.handle_query)
+        self.region_timer = QTimer()  # 10秒检测区域
+        self.region_timer.timeout.connect(self.check_regions)
+        
+        # 区域相关
+        self.regions = []
+        self.region_cars = {}
+        
         self.initUI()
+        self.load_config()
     
     def initUI(self):
         self.setWindowTitle('车辆定位监控系统')
-        self.setGeometry(100, 100, 1400, 800)
+        self.setGeometry(100, 100, 1500, 850)
         
-        # 中央窗口
         central_widget = QWidget()
         self.setCentralWidget(central_widget)
         main_layout = QVBoxLayout(central_widget)
         
-        # 登录区域
+        # ===== 登录区域 =====
         login_group = QGroupBox("登录")
         login_layout = QHBoxLayout()
-        
         login_layout.addWidget(QLabel("账号:"))
         self.username_input = QLineEdit()
-        self.username_input.setPlaceholderText("请输入账号")
         self.username_input.setText("深金川")
         login_layout.addWidget(self.username_input)
-        
         login_layout.addWidget(QLabel("密码:"))
         self.password_input = QLineEdit()
-        self.password_input.setPlaceholderText("请输入密码")
         self.password_input.setEchoMode(QLineEdit.Password)
         self.password_input.setText("123456")
         login_layout.addWidget(self.password_input)
-        
         self.login_btn = QPushButton("登录")
         self.login_btn.clicked.connect(self.handle_login)
         login_layout.addWidget(self.login_btn)
-        
         self.status_label = QLabel("未登录")
         login_layout.addWidget(self.status_label)
-        
         login_layout.addStretch()
         login_group.setLayout(login_layout)
         main_layout.addWidget(login_group)
         
-        # 地址输入区域
-        addr_group = QGroupBox("地址监控设置")
-        addr_layout = QGridLayout()
+        # ===== WebSocket控制 =====
+        ws_group = QGroupBox("WebSocket控制")
+        ws_layout = QHBoxLayout()
+        
+        self.ws_status_label = QLabel("未连接")
+        ws_layout.addWidget(self.ws_status_label)
+        
+        self.refresh_clients_btn = QPushButton("刷新客户端列表")
+        self.refresh_clients_btn.clicked.connect(self.refresh_client_list)
+        self.refresh_clients_btn.setEnabled(False)
+        ws_layout.addWidget(self.refresh_clients_btn)
+        
+        ws_layout.addWidget(QLabel("装车客户端:"))
+        self.send_client_combo = QComboBox()
+        self.send_client_combo.setMinimumWidth(120)
+        ws_layout.addWidget(self.send_client_combo)
+        
+        ws_layout.addWidget(QLabel("卸车客户端:"))
+        self.receive_client_combo = QComboBox()
+        self.receive_client_combo.setMinimumWidth(120)
+        ws_layout.addWidget(self.receive_client_combo)
+        
+        # 测试按钮
+        self.test_send_btn = QPushButton("测试发")
+        self.test_send_btn.clicked.connect(self.test_send)
+        self.test_send_btn.setEnabled(False)
+        ws_layout.addWidget(self.test_send_btn)
+        
+        self.test_receive_btn = QPushButton("测试收")
+        self.test_receive_btn.clicked.connect(self.test_receive)
+        self.test_receive_btn.setEnabled(False)
+        ws_layout.addWidget(self.test_receive_btn)
+        
+        ws_layout.addStretch()
+        ws_group.setLayout(ws_layout)
+        main_layout.addWidget(ws_group)
+        
+        # ===== 文件夹选择 =====
+        folder_group = QGroupBox("文件路径设置")
+        folder_layout = QHBoxLayout()
+        
+        folder_layout.addWidget(QLabel("选择文件夹:"))
+        self.folder_path_input = QLineEdit()
+        self.folder_path_input.setPlaceholderText("请选择文件夹路径")
+        self.folder_path_input.setMinimumWidth(400)
+        folder_layout.addWidget(self.folder_path_input)
+        
+        self.browse_folder_btn = QPushButton("浏览文件夹")
+        self.browse_folder_btn.clicked.connect(self.browse_folder)
+        folder_layout.addWidget(self.browse_folder_btn)
+        
+        self.clear_folder_btn = QPushButton("清空")
+        self.clear_folder_btn.clicked.connect(self.clear_folder)
+        folder_layout.addWidget(self.clear_folder_btn)
         
-        addr_layout.addWidget(QLabel("装车地址:"), 0, 0)
-        self.load_addr_input = QLineEdit()
-        self.load_addr_input.setPlaceholderText("请输入装车地址关键词")
-        addr_layout.addWidget(self.load_addr_input, 0, 1)
+        folder_layout.addStretch()
+        folder_group.setLayout(folder_layout)
+        main_layout.addWidget(folder_group)
         
-        addr_layout.addWidget(QLabel("卸车地址:"), 0, 2)
-        self.unload_addr_input = QLineEdit()
-        self.unload_addr_input.setPlaceholderText("请输入卸车地址关键词")
-        addr_layout.addWidget(self.unload_addr_input, 0, 3)
+        # ===== 区域监控设置 =====
+        region_group = QGroupBox("区域监控设置")
+        region_layout = QGridLayout()
+        
+        region_layout.addWidget(QLabel("装车区域:"), 0, 0)
+        self.load_region_combo = QComboBox()
+        self.load_region_combo.setMinimumWidth(150)
+        region_layout.addWidget(self.load_region_combo, 0, 1)
+        
+        region_layout.addWidget(QLabel("卸车区域:"), 0, 2)
+        self.unload_region_combo = QComboBox()
+        self.unload_region_combo.setMinimumWidth(150)
+        region_layout.addWidget(self.unload_region_combo, 0, 3)
+        
+        self.refresh_region_btn = QPushButton("刷新区域")
+        self.refresh_region_btn.clicked.connect(self.load_regions)
+        self.refresh_region_btn.setEnabled(False)
+        region_layout.addWidget(self.refresh_region_btn, 0, 4)
         
-        # 控制按钮
         self.start_btn = QPushButton("启动监控")
         self.start_btn.clicked.connect(self.start_monitoring)
         self.start_btn.setEnabled(False)
-        addr_layout.addWidget(self.start_btn, 0, 4)
+        region_layout.addWidget(self.start_btn, 0, 5)
         
         self.stop_btn = QPushButton("停止监控")
         self.stop_btn.clicked.connect(self.stop_monitoring)
         self.stop_btn.setEnabled(False)
-        addr_layout.addWidget(self.stop_btn, 0, 5)
+        region_layout.addWidget(self.stop_btn, 0, 6)
         
-        self.manual_query_btn = QPushButton("手动刷新")
-        self.manual_query_btn.clicked.connect(self.handle_query)
-        self.manual_query_btn.setEnabled(False)
-        addr_layout.addWidget(self.manual_query_btn, 0, 6)
+        region_layout.addWidget(QLabel("检测间隔:10秒"), 0, 7)
         
-        addr_layout.addWidget(QLabel("刷新间隔:5秒"), 0, 7)
+        # 区域状态显示条
+        self.region_status_label = QLabel("请选择区域")
+        self.region_status_label.setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }")
+        region_layout.addWidget(self.region_status_label, 1, 0, 1, 8)
         
-        addr_group.setLayout(addr_layout)
-        main_layout.addWidget(addr_group)
+        region_group.setLayout(region_layout)
+        main_layout.addWidget(region_group)
         
-        # 分割器:表格和日志
+        # ===== 车辆列表 =====
         splitter = QSplitter(Qt.Vertical)
         
-        # 车辆信息表格
         table_widget = QWidget()
         table_layout = QVBoxLayout(table_widget)
         table_layout.setContentsMargins(0, 0, 0, 0)
         
         self.table = QTableWidget()
-        self.table.setColumnCount(5)
-        self.table.setHorizontalHeaderLabels(["选择", "车队名", "车牌号", "在线状态", "定位地址"])
-        self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.Stretch)
+        # 修改列数:增加"扫码"列和"操作"列
+        self.table.setColumnCount(7)
+        self.table.setHorizontalHeaderLabels(["启用", "扫码", "车队名", "车牌号", "状态", "定位地址", "操作"])
+        self.table.horizontalHeader().setSectionResizeMode(5, QHeaderView.Stretch)
         self.table.setAlternatingRowColors(True)
         table_layout.addWidget(self.table)
         
-        # 表格按钮
-        table_btn_layout = QHBoxLayout()
-        self.select_all_btn = QPushButton("全选")
-        self.select_all_btn.clicked.connect(self.select_all)
-        table_btn_layout.addWidget(self.select_all_btn)
+        btn_layout = QHBoxLayout()
+        self.select_all_btn = QPushButton("全选启用")
+        self.select_all_btn.clicked.connect(self.select_all_enable)
+        btn_layout.addWidget(self.select_all_btn)
+        self.deselect_all_btn = QPushButton("取消全选启用")
+        self.deselect_all_btn.clicked.connect(self.deselect_all_enable)
+        btn_layout.addWidget(self.deselect_all_btn)
         
-        self.deselect_all_btn = QPushButton("取消全选")
-        self.deselect_all_btn.clicked.connect(self.deselect_all)
-        table_btn_layout.addWidget(self.deselect_all_btn)
+        self.select_all_scan_btn = QPushButton("全选扫码")
+        self.select_all_scan_btn.clicked.connect(self.select_all_scan)
+        btn_layout.addWidget(self.select_all_scan_btn)
+        self.deselect_all_scan_btn = QPushButton("取消全选扫码")
+        self.deselect_all_scan_btn.clicked.connect(self.deselect_all_scan)
+        btn_layout.addWidget(self.deselect_all_scan_btn)
         
-        table_btn_layout.addStretch()
-        table_layout.addLayout(table_btn_layout)
+        btn_layout.addStretch()
+        table_layout.addLayout(btn_layout)
         
         splitter.addWidget(table_widget)
         
-        # 日志输出框
+        # ===== 日志 =====
         log_widget = QWidget()
         log_layout = QVBoxLayout(log_widget)
         log_layout.setContentsMargins(0, 0, 0, 0)
-        
-        log_label = QLabel("日志输出")
-        log_layout.addWidget(log_label)
-        
+        log_layout.addWidget(QLabel("日志"))
         self.log_output = QTextEdit()
         self.log_output.setReadOnly(True)
-        # 限制日志行数 - 使用document().setMaximumBlockCount()
-        self.log_output.document().setMaximumBlockCount(1000)
+        self.log_output.document().setMaximumBlockCount(500)
         log_layout.addWidget(self.log_output)
         
         splitter.addWidget(log_widget)
-        splitter.setSizes([500, 300])
-        
+        splitter.setSizes([450, 300])
         main_layout.addWidget(splitter)
         
-        # 状态栏
         self.statusBar().showMessage("就绪")
-        
-        # 初始化触发记录
         self.triggered_records = {}
     
+    def get_value_with_plate(self, car_plate):
+        """生成带车牌的文件路径: 文件夹路径/车牌.jpg"""
+        folder = self.folder_path_input.text().strip()
+        if not folder:
+            return ""
+        # 确保路径格式正确
+        folder = folder.rstrip('/\\')
+        return f"{folder}/{car_plate}.jpg"
+    
+    def get_exe_path_with_plate(self, car_plate):
+        """生成可执行文件路径: 文件夹路径/车牌号/运行程序.exe"""
+        folder = self.folder_path_input.text().strip()
+        if not folder:
+            return ""
+        # 确保路径格式正确
+        folder = folder.rstrip('/\\')
+        return f"{folder}/{car_plate}/运行程序.exe"
+    
+    # ===== 文件夹选择 =====
+    def browse_folder(self):
+        """浏览并选择文件夹"""
+        folder_path = QFileDialog.getExistingDirectory(
+            self, 
+            "选择文件夹", 
+            self.folder_path_input.text() if self.folder_path_input.text() else "",
+            QFileDialog.ShowDirsOnly
+        )
+        
+        if folder_path:
+            self.folder_path = folder_path
+            self.folder_path_input.setText(folder_path)
+            self.add_log(f"📁 已选择文件夹: {folder_path}")
+            self.save_config()
+    
+    def clear_folder(self):
+        """清空文件夹路径"""
+        self.folder_path = ""
+        self.folder_path_input.clear()
+        self.add_log("已清空文件夹路径")
+        self.save_config()
+    
+    # ===== WebSocket连接 =====
+    def connect_websocket(self, device_id):
+        """连接WebSocket服务器"""
+        if self.ws_client and self.ws_client.is_running:
+            return
+        
+        self.ws_status_label.setText("连接中...")
+        self.ws_status_label.setStyleSheet("QLabel { color: orange; }")
+        
+        # 创建WebSocket客户端,传入获取文件夹路径的回调
+        self.ws_client = WebSocketClient(get_folder_callback=self.get_folder_path_for_ws)
+        self.ws_client.connected.connect(self.on_ws_connected)
+        self.ws_client.disconnected.connect(self.on_ws_disconnected)
+        self.ws_client.message_received.connect(self.on_ws_message)
+        self.ws_client.error.connect(self.on_ws_error)
+        self.ws_client.connect(device_id)
+
+    def get_folder_path_for_ws(self):
+        """提供给WebSocket客户端的文件夹路径获取方法"""
+        return self.folder_path_input.text().strip()
+    
+    def on_ws_connected(self):
+        """WebSocket连接成功"""
+        self.refresh_clients_btn.setEnabled(True)
+        self.test_send_btn.setEnabled(True)
+        self.test_receive_btn.setEnabled(True)
+        self.ws_status_label.setText("已连接")
+        self.ws_status_label.setStyleSheet("QLabel { color: green; }")
+        self.add_log("✅ WebSocket连接成功")
+        
+        # 连接成功后获取客户端列表
+        QTimer.singleShot(1000, self.refresh_client_list)
+    
+    def on_ws_disconnected(self):
+        """WebSocket连接断开"""
+        self.refresh_clients_btn.setEnabled(False)
+        self.test_send_btn.setEnabled(False)
+        self.test_receive_btn.setEnabled(False)
+        self.ws_status_label.setText("已断开")
+        self.ws_status_label.setStyleSheet("QLabel { color: red; }")
+        self.add_log("⚠️ WebSocket连接断开")
+    
+    def on_ws_error(self, error_msg):
+        """WebSocket错误"""
+        self.add_log(f"❌ WebSocket错误: {error_msg}")
+    
+    def on_ws_message(self, data):
+        """处理WebSocket消息"""
+        msg_type = data.get('type')
+        self.add_log(json.dumps(data, ensure_ascii=False))
+        if msg_type == 'getList':
+            # 客户端列表
+            clients = data.get('value', [])
+            self.on_client_list_received(clients)
+        elif msg_type == 'userInfo':
+            self.add_log(f"✅ 用户信息已获取: {data.get('value', {}).get('userID')}")
+        elif msg_type == 'imgSaved':
+            # 图片保存成功,触发扫码任务
+            self.add_log(f"📸 图片已保存: {data.get('filepath', '')}")
+            self.start_scan_tasks()
+        elif msg_type == 'drive':  # 收到"开车"命令
+            # 外部WebSocket发来的开车命令
+            car_plate = data.get('value', '')
+            if car_plate:
+                self.add_log(f"📤 收到外部WebSocket开车命令,车辆: {car_plate}")
+                # 发送2001指令
+                self.send_cmd_to_vehicle(car_plate, "2001")
+                # 继续处理下一辆车
+                self.process_next_scan()
+            else:
+                self.add_log(f"⚠️ 收到开车命令但车牌为空")
+        
+    
+    def start_scan_tasks(self):
+        """开始执行扫码任务"""
+        # 如果正在处理,忽略新的请求
+        if self.is_processing_scan:
+            self.add_log("⏳ 扫码任务正在处理中,忽略本次请求")
+            return
+        
+        # 获取所有勾选了扫码的车辆
+        scan_vehicles = []
+        for row in range(self.table.rowCount()):
+            scan_checkbox = self.table.cellWidget(row, 1)
+            if scan_checkbox and scan_checkbox.isChecked():
+                car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
+                if car_plate:
+                    scan_vehicles.append(car_plate)
+        
+        if not scan_vehicles:
+            self.add_log("⚠️ 没有勾选扫码的车辆")
+            return
+        
+        # 初始化队列
+        self.scan_task_queue = scan_vehicles.copy()
+        self.is_processing_scan = True
+        
+        self.add_log(f"🚀 开始扫码任务,共 {len(scan_vehicles)} 辆车")
+        self.add_log(f"📋 车辆列表: {', '.join(scan_vehicles)}")
+        
+        # 立即处理第一个
+        self.process_next_scan()
+
+    def process_next_scan(self):
+        """处理下一个扫码任务"""
+        # 如果监控已停止,不再处理
+        if not self.is_monitoring:
+            self.add_log("⏹️ 监控已停止,取消扫码任务")
+            self.is_processing_scan = False
+            self.scan_task_queue = []
+            self.current_scan_plate = None
+            self.scan_timer.stop()
+            return
+        
+        if not self.scan_task_queue:
+            # 所有任务完成
+            self.is_processing_scan = False
+            self.add_log("✅ 所有扫码任务已完成")
+            self.statusBar().showMessage("扫码任务全部完成")
+            self.scan_timer.stop()
+            self.current_scan_plate = None
+            return
+        
+        # 取出第一个车辆
+        car_plate = self.scan_task_queue.pop(0)
+        self.current_scan_plate = car_plate
+        self.add_log(f"🔄 正在处理扫码车辆: {car_plate} (剩余 {len(self.scan_task_queue)} 辆)")
+        
+        # 执行测试发车(标记为来自扫码任务)
+        self.on_test_drive(car_plate, from_scan=True)
+        
+        # 启动定时器,1分钟后发送指令2001,然后处理下一辆
+        # 使用 QTimer.singleShot 单次定时器,传入当前车牌
+        self.add_log(f"⏳ 等待外部WebSocket通知发送2001指令到: {car_plate}")
+        # QTimer.singleShot(60000, lambda: self.send_2001_and_next(car_plate))
+
+    def send_2001_and_next(self, car_plate):
+        """发送2001指令给指定车辆,然后继续处理下一个"""
+        self.add_log(f"📤 发送指令 2001 到车辆: {car_plate}")
+        self.send_cmd_to_vehicle(car_plate, "2001")
+        
+        # 发送完指令后,继续处理下一辆(立即执行,不再等待)
+        self.process_next_scan()
+
+    def check_scan_complete(self):
+        """检查扫码任务是否完成"""
+        if not self.scan_task_queue and self.is_processing_scan:
+            self.is_processing_scan = False
+            self.add_log("✅ 所有扫码任务已完成")
+            self.statusBar().showMessage("扫码任务全部完成")
+
+    def on_client_list_received(self, clients):
+        """处理客户端列表,过滤掉 admin 为 true 的客户端"""
+        self.online_clients = {}
+        self.send_client_combo.clear()
+        self.receive_client_combo.clear()
+        
+        # 添加默认选项
+        self.send_client_combo.addItem("请选择装车客户端")
+        self.receive_client_combo.addItem("请选择卸车客户端")
+        
+        for client in clients:
+            # 过滤掉 admin 为 true 的客户端(这些是管理端,不是扫码客户端)
+            if client.get('admin', False):
+                continue
+            
+            client_id = client.get('id')
+            user_id = client.get('userID')
+            is_active = client.get('active', False)
+            
+            if client_id and user_id and is_active:
+                # 使用 userID 作为键存储客户端信息
+                self.online_clients[user_id] = client
+                # 显示时显示 id,但存储 userID
+                display_name = f"{client_id}"
+                self.send_client_combo.addItem(display_name, user_id)
+                self.receive_client_combo.addItem(display_name, user_id)
+                self.add_log(f"发现客户端: {client_id} (userID: {user_id})")
+        
+        self.add_log(f"✅ 已加载 {len(self.online_clients)} 个在线扫码客户端")
+    
+    def refresh_client_list(self):
+        """刷新客户端列表"""
+        if not self.ws_client:
+            return
+        
+        if not self.ws_client.is_running:
+            self.add_log("❌ WebSocket未连接")
+            return
+            
+        if self.ws_client.send("getList"):
+            self.add_log("📡 正在获取客户端列表...")
+        else:
+            self.add_log("❌ 发送获取列表命令失败")
+    
+    def test_send(self):
+        """测试发送(装车)"""
+        if not self.ws_client or not self.ws_client.is_running:
+            self.add_log("❌ WebSocket未连接")
+            return
+        
+        user_id = self.send_client_combo.currentData()
+        if not user_id:
+            self.add_log("❌ 请选择装车客户端")
+            return
+        
+        # 测试时使用一个示例车牌
+        test_plate = "测试车牌"
+        value = self.get_value_with_plate(test_plate)
+        
+        if not value:
+            self.add_log("⚠️ 未选择文件夹,将发送空值")
+        
+        if self.ws_client.send("发货", value, user_id):
+            self.add_log(f"✅ 测试发送(装车)指令已发送到 {user_id},value: {value}")
+        else:
+            self.add_log("❌ 测试发送失败")
+    
+    def test_receive(self):
+        """测试接收(卸车)"""
+        if not self.ws_client or not self.ws_client.is_running:
+            self.add_log("❌ WebSocket未连接")
+            return
+        
+        user_id = self.receive_client_combo.currentData()
+        if not user_id:
+            self.add_log("❌ 请选择卸车客户端")
+            return
+        
+        # 测试时使用一个示例车牌
+        test_plate = "测试车牌"
+        value = self.get_value_with_plate(test_plate)
+        
+        if not value:
+            self.add_log("⚠️ 未选择文件夹,将发送空值")
+        
+        if self.ws_client.send("收货", value, user_id):
+            self.add_log(f"✅ 测试接收(卸车)指令已发送到 {user_id},value: {value}")
+        else:
+            self.add_log("❌ 测试接收失败")
+    
+    # ===== 登录 =====
     def handle_login(self):
         username = self.username_input.text().strip()
         password = self.password_input.text().strip()
-        
         if not username or not password:
             QMessageBox.warning(self, "警告", "请输入账号和密码")
             return
-        
         self.login_btn.setEnabled(False)
         self.status_label.setText("登录中...")
-        self.statusBar().showMessage("正在登录...")
-        
         self.login_thread = LoginThread(username, password)
         self.login_thread.finished.connect(self.on_login_success)
         self.login_thread.error.connect(self.on_login_error)
@@ -227,33 +801,88 @@ class MainWindow(QMainWindow):
     
     def on_login_success(self, data):
         self.session_id = data.get('sessionId')
-        self.username = data.get('userName', '')
-        
+        self.username = self.username_input.text().strip()
         if self.session_id:
             self.login_btn.setEnabled(True)
             self.status_label.setText(f"已登录: {self.username}")
-            self.manual_query_btn.setEnabled(True)
+            self.refresh_region_btn.setEnabled(True)
             self.start_btn.setEnabled(True)
-            self.statusBar().showMessage(f"登录成功,Session ID: {self.session_id[:20]}...")
-            
-            # 登录成功后自动查询
+            self.statusBar().showMessage("登录成功")
+            self.load_regions()
             self.handle_query()
+            
+            # 自动连接WebSocket,使用用户名作为ID
+            self.connect_websocket(self.username)
+            self.save_config()
         else:
             self.on_login_error("登录响应中没有sessionId")
     
     def on_login_error(self, error_msg):
         self.login_btn.setEnabled(True)
         self.status_label.setText("登录失败")
-        self.statusBar().showMessage("登录失败")
         QMessageBox.critical(self, "错误", error_msg)
     
-    def handle_query(self):
+    # ===== 加载区域 =====
+    def load_regions(self):
         if not self.session_id:
             QMessageBox.warning(self, "警告", "请先登录")
             return
         
-        self.manual_query_btn.setEnabled(False)
-        self.statusBar().showMessage("正在查询车辆定位...")
+        self.refresh_region_btn.setEnabled(False)
+        self.statusBar().showMessage("正在加载区域...")
+        
+        self.region_thread = RegionQueryThread(self.session_id)
+        self.region_thread.finished.connect(self.on_regions_loaded)
+        self.region_thread.error.connect(self.on_regions_error)
+        self.region_thread.start()
+    
+    def on_regions_loaded(self, data):
+        self.refresh_region_btn.setEnabled(True)
+        self.regions = data.get('list', [])
+        
+        self.load_region_combo.clear()
+        self.unload_region_combo.clear()
+        self.load_region_combo.addItem("请选择装车区域")
+        self.unload_region_combo.addItem("请选择卸车区域")
+        
+        for region in self.regions:
+            region_name = region.get('regionName', '未知')
+            region_id = region.get('regionId', '')
+            display = f"{region_name}"
+            self.load_region_combo.addItem(display, region_id)
+            self.unload_region_combo.addItem(display, region_id)
+            self.region_cars[region_id] = region.get('carList', [])
+        
+        self.update_region_status()
+        self.statusBar().showMessage(f"加载了 {len(self.regions)} 个区域")
+        self.add_log(f"✅ 加载了 {len(self.regions)} 个区域")
+    
+    def on_regions_error(self, error_msg):
+        self.refresh_region_btn.setEnabled(True)
+        QMessageBox.critical(self, "错误", error_msg)
+    
+    def update_region_status(self):
+        """更新区域状态显示条"""
+        load_text = self.load_region_combo.currentText() if self.load_region_combo.currentIndex() > 0 else "未选择"
+        unload_text = self.unload_region_combo.currentText() if self.unload_region_combo.currentIndex() > 0 else "未选择"
+        
+        # 获取车辆数量
+        load_cars = 0
+        unload_cars = 0
+        if self.load_region_combo.currentIndex() > 0:
+            region_id = self.load_region_combo.currentData()
+            load_cars = len(self.region_cars.get(region_id, []))
+        if self.unload_region_combo.currentIndex() > 0:
+            region_id = self.unload_region_combo.currentData()
+            unload_cars = len(self.region_cars.get(region_id, []))
+        
+        status = f"🚛 装车: {load_text} ({load_cars}辆)  |  📦 卸车: {unload_text} ({unload_cars}辆)"
+        self.region_status_label.setText(status)
+    
+    # ===== 查询车辆列表 =====
+    def handle_query(self):
+        if not self.session_id:
+            return
         
         self.query_thread = QueryThread(self.session_id)
         self.query_thread.finished.connect(self.on_query_success)
@@ -261,174 +890,445 @@ class MainWindow(QMainWindow):
         self.query_thread.start()
     
     def on_query_success(self, data):
-        self.manual_query_btn.setEnabled(True)
-        
         car_list = data.get('list', [])
         self.car_data = car_list
-        
-        # 保存当前勾选状态
         self.save_check_state()
-        
         self.table.setRowCount(len(car_list))
         
         for row, car in enumerate(car_list):
             car_plate = car.get('carPlate', '')
             
-            # 选 - 恢复保存的状态
-            checkbox = QCheckBox()
-            checkbox.setChecked(self.car_checked_state.get(car_plate, False))
-            checkbox.stateChanged.connect(lambda state, plate=car_plate: self.on_check_changed(plate, state))
-            self.table.setCellWidget(row, 0, checkbox)
+            # 第0列:启用复选框
+            enable_checkbox = QCheckBox()
+            enable_checkbox.setChecked(self.car_checked_state.get(car_plate, False))
+            enable_checkbox.stateChanged.connect(lambda state, plate=car_plate: self.on_enable_changed(plate, state))
+            self.table.setCellWidget(row, 0, enable_checkbox)
             
-            # 车队名
-            team_name = car.get('teamName', '')
-            self.table.setItem(row, 1, QTableWidgetItem(team_name))
+            # 第1列:扫码复选框
+            scan_checkbox = QCheckBox()
+            scan_checkbox.setChecked(self.car_scan_state.get(car_plate, False))
+            scan_checkbox.stateChanged.connect(lambda state, plate=car_plate: self.on_scan_changed(plate, state))
+            self.table.setCellWidget(row, 1, scan_checkbox)
             
-            # 车牌号
-            self.table.setItem(row, 2, QTableWidgetItem(car_plate))
+            # 第2列:车队名
+            self.table.setItem(row, 2, QTableWidgetItem(car.get('teamName', '')))
             
-            # 在线状态
-            state_cn = car.get('stateCn', '')
-            self.table.setItem(row, 3, QTableWidgetItem(state_cn))
+            # 第3列:车牌号
+            self.table.setItem(row, 3, QTableWidgetItem(car_plate))
             
-            # 定位地址
-            addr = car.get('addr', '')
-            self.table.setItem(row, 4, QTableWidgetItem(addr))
-        
-        self.statusBar().showMessage(f"查询成功,共 {len(car_list)} 辆车")
+            # 第4列:状态
+            self.table.setItem(row, 4, QTableWidgetItem(car.get('stateCn', '')))
+            
+            # 第5列:定位地址
+            self.table.setItem(row, 5, QTableWidgetItem(car.get('addr', '')))
+            
+            # 第6列:操作按钮(测试发车)
+            test_btn = QPushButton("测试发车")
+            test_btn.clicked.connect(lambda checked, plate=car_plate: self.on_test_drive(plate))
+            self.table.setCellWidget(row, 6, test_btn)
         
-        # 如果正在监控,进行地址匹配检查
-        if self.is_monitoring:
-            self.check_address_match()
+        self.statusBar().showMessage(f"车辆列表更新: {len(car_list)} 辆车")
     
     def on_query_error(self, error_msg):
-        self.manual_query_btn.setEnabled(True)
-        self.statusBar().showMessage("查询失败")
         QMessageBox.critical(self, "错误", error_msg)
     
+    # ===== 表格操作 =====
     def save_check_state(self):
-        """保存当前表格中的勾选状态"""
+        """保存启用和扫码状态"""
         for row in range(self.table.rowCount()):
-            checkbox = self.table.cellWidget(row, 0)
-            if checkbox:
-                car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
+            # 启用复选框
+            enable_checkbox = self.table.cellWidget(row, 0)
+            if enable_checkbox:
+                car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
                 if car_plate:
-                    self.car_checked_state[car_plate] = checkbox.isChecked()
+                    self.car_checked_state[car_plate] = enable_checkbox.isChecked()
+            
+            # 扫码复选框
+            scan_checkbox = self.table.cellWidget(row, 1)
+            if scan_checkbox:
+                car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
+                if car_plate:
+                    self.car_scan_state[car_plate] = scan_checkbox.isChecked()
     
-    def on_check_changed(self, car_plate, state):
-        """勾选框状态变化时保存"""
+    def on_enable_changed(self, car_plate, state):
+        """启用复选框状态变化"""
         self.car_checked_state[car_plate] = (state == Qt.Checked)
     
-    def select_all(self):
+    def on_scan_changed(self, car_plate, state):
+        """扫码复选框状态变化"""
+        self.car_scan_state[car_plate] = (state == Qt.Checked)
+    
+    def select_all_enable(self):
+        """全选启用"""
         for row in range(self.table.rowCount()):
             checkbox = self.table.cellWidget(row, 0)
             if checkbox:
                 checkbox.setChecked(True)
-                car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
+                car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
                 if car_plate:
                     self.car_checked_state[car_plate] = True
     
-    def deselect_all(self):
+    def deselect_all_enable(self):
+        """取消全选启用"""
         for row in range(self.table.rowCount()):
             checkbox = self.table.cellWidget(row, 0)
             if checkbox:
                 checkbox.setChecked(False)
-                car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
+                car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
                 if car_plate:
                     self.car_checked_state[car_plate] = False
     
+    def select_all_scan(self):
+        """全选扫码"""
+        for row in range(self.table.rowCount()):
+            checkbox = self.table.cellWidget(row, 1)
+            if checkbox:
+                checkbox.setChecked(True)
+                car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
+                if car_plate:
+                    self.car_scan_state[car_plate] = True
+    
+    def deselect_all_scan(self):
+        """取消全选扫码"""
+        for row in range(self.table.rowCount()):
+            checkbox = self.table.cellWidget(row, 1)
+            if checkbox:
+                checkbox.setChecked(False)
+                car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
+                if car_plate:
+                    self.car_scan_state[car_plate] = False
+    
+    # ===== 测试发车功能 =====
+    def on_test_drive(self, car_plate, from_scan=False):
+        """测试发车按钮点击事件"""
+        # 如果是扫码任务且监控已停止,不再执行
+        if from_scan and not self.is_monitoring:
+            self.add_log(f"⏹️ 监控已停止,取消测试发车: {car_plate}")
+            return
+        if not car_plate:
+            if not from_scan:
+                self.add_log("❌ 车牌号为空,无法执行测试发车")
+            return
+        
+        # 获取可执行文件路径
+        exe_path = self.get_exe_path_with_plate(car_plate)
+        
+        if not exe_path:
+            if not from_scan:
+                self.add_log(f"❌ 未选择文件夹,无法执行测试发车: {car_plate}")
+                QMessageBox.warning(self, "警告", "请先选择文件夹路径")
+            else:
+                self.add_log(f"❌ 未选择文件夹,跳过: {car_plate}")
+            # 继续处理下一个
+            self.scan_timer.start(1000)
+            return
+        
+        # 检查文件是否存在
+        if not os.path.exists(exe_path):
+            if not from_scan:
+                self.add_log(f"❌ 可执行文件不存在: {exe_path}")
+                QMessageBox.warning(self, "错误", f"可执行文件不存在:\n{exe_path}")
+            else:
+                self.add_log(f"❌ 可执行文件不存在,跳过: {car_plate}")
+            # 继续处理下一个
+            self.scan_timer.start(1000)
+            return
+        
+        try:
+            # 获取exe所在目录作为工作目录
+            exe_dir = os.path.dirname(exe_path)
+            
+            if from_scan:
+                self.add_log(f"🚀 [扫码任务] 执行测试发车: {car_plate}")
+            else:
+                self.add_log(f"🚀 执行测试发车: {car_plate}")
+                self.add_log(f"📂 执行路径: {exe_path}")
+                self.add_log(f"📁 工作目录: {exe_dir}")
+            
+            # 使用 ShellExecute 启动
+            import ctypes
+            result = ctypes.windll.shell32.ShellExecuteW(
+                None,           # 父窗口句柄
+                "open",         # 操作类型
+                exe_path,       # 要打开的程序路径
+                "",             # 命令行参数
+                exe_dir,        # 工作目录
+                1               # 窗口显示状态 (1=正常显示)
+            )
+            
+            # ShellExecuteW 返回值 > 32 表示成功
+            if result > 32:
+                self.add_log(f"✅ [扫码任务] 测试发车启动成功: {car_plate}")
+                self.statusBar().showMessage(f"测试发车已启动: {car_plate}")
+            else:
+                self.add_log(f"❌ [扫码任务] 启动失败: {car_plate},错误码: {result}")
+            
+        except Exception as e:
+            self.add_log(f"❌ [扫码任务] 执行失败 {car_plate}: {str(e)}")
+        
+        # 注意:这里的定时器会在 process_next_scan 中启动,不需要在这里启动
+    
+    # ===== 监控 =====
     def start_monitoring(self):
-        """启动监控"""
-        load_addr = self.load_addr_input.text().strip()
-        unload_addr = self.unload_addr_input.text().strip()
+        load_index = self.load_region_combo.currentIndex()
+        unload_index = self.unload_region_combo.currentIndex()
         
-        if not load_addr and not unload_addr:
-            QMessageBox.warning(self, "警告", "请至少输入一个地址关键词")
+        if load_index <= 0 and unload_index <= 0:
+            QMessageBox.warning(self, "警告", "请选择装车区域和/或卸车区域")
             return
         
         if not self.session_id:
             QMessageBox.warning(self, "警告", "请先登录")
             return
         
+        if self.send_client_combo.currentIndex() <= 0:
+            QMessageBox.warning(self, "警告", "请选择装车客户端")
+            return
+        
+        if self.receive_client_combo.currentIndex() <= 0:
+            QMessageBox.warning(self, "警告", "请选择卸车客户端")
+            return
+        
+        if not self.ws_client or not self.ws_client.is_running:
+            QMessageBox.warning(self, "警告", "WebSocket未连接")
+            return
+        
+        # 检查是否选择了文件夹
+        if not self.folder_path_input.text().strip():
+            reply = QMessageBox.question(
+                self, 
+                "确认", 
+                "未选择文件夹,将发送空值,是否继续?",
+                QMessageBox.Yes | QMessageBox.No,
+                QMessageBox.No
+            )
+            if reply == QMessageBox.No:
+                return
+        
         self.is_monitoring = True
         self.start_btn.setEnabled(False)
         self.stop_btn.setEnabled(True)
-        self.manual_query_btn.setEnabled(False)
+        self.refresh_region_btn.setEnabled(False)
         
-        # 清空触发记录
         self.triggered_records = {}
         
+        load_region = self.load_region_combo.currentText() if load_index > 0 else "无"
+        unload_region = self.unload_region_combo.currentText() if unload_index > 0 else "无"
+        
+        load_client = self.send_client_combo.currentText()
+        unload_client = self.receive_client_combo.currentText()
+        
+        folder = self.folder_path_input.text().strip() or "无"
+        
         self.add_log("=" * 50)
-        self.add_log(f"监控已启动 - 装车地址: {load_addr or '无'}, 卸车地址: {unload_addr or '无'}")
+        self.add_log(f"监控启动 - 装车区域: {load_region}, 卸车区域: {unload_region}")
+        self.add_log(f"装车客户端: {load_client}, 卸车客户端: {unload_client}")
+        self.add_log(f"文件夹路径: {folder}")
         self.add_log("=" * 50)
         
-        # 立即执行一次检查
-        self.handle_query()
+        # 启动两个定时器
+        self.refresh_timer.start(10000)
+        self.region_timer.start(10000)
         
-        # 启动定时器,每5秒刷新一次
-        self.refresh_timer.start(5000)
         self.statusBar().showMessage("监控运行中...")
     
     def stop_monitoring(self):
-        """停止监控"""
         self.is_monitoring = False
         self.refresh_timer.stop()
+        self.region_timer.stop()
         self.start_btn.setEnabled(True)
         self.stop_btn.setEnabled(False)
-        self.manual_query_btn.setEnabled(True)
+        self.refresh_region_btn.setEnabled(True)
+        
+        # 停止扫码任务
+        self.scan_timer.stop()
+        self.is_processing_scan = False
+        self.scan_task_queue = []
+        self.current_scan_plate = None
         
-        self.add_log("=" * 50)
         self.add_log("监控已停止")
-        self.add_log("=" * 50)
         self.statusBar().showMessage("监控已停止")
     
-    def auto_refresh(self):
-        """自动刷新"""
-        if self.is_monitoring and self.session_id:
-            self.handle_query()
+    # ===== 区域检测 =====
+    def check_regions(self):
+        """检测区域车辆"""
+        if not self.is_monitoring:
+            return
+        
+        load_region_id = self.load_region_combo.currentData() if self.load_region_combo.currentIndex() > 0 else None
+        unload_region_id = self.unload_region_combo.currentData() if self.unload_region_combo.currentIndex() > 0 else None
+        
+        if not load_region_id and not unload_region_id:
+            return
+        
+        self.region_thread = RegionQueryThread(self.session_id)
+        self.region_thread.finished.connect(lambda data: self.on_region_check(data, load_region_id, unload_region_id))
+        self.region_thread.error.connect(lambda err: self.add_log(f"❌ 检测区域失败: {err}"))
+        self.region_thread.start()
     
-    def check_address_match(self):
-        """检查地址匹配"""
-        load_keyword = self.load_addr_input.text().strip()
-        unload_keyword = self.unload_addr_input.text().strip()
+    def on_region_check(self, data, load_region_id, unload_region_id):
+        """处理区域检测结果"""
+        regions = data.get('list', [])
         current_time = datetime.now()
+        self.add_log(json.dumps(regions))
+        for region in regions:
+            region_id = region.get('regionId')
+            region_name = region.get('regionName', '未知')
+            car_list = region.get('carList', [])
+            
+            # 更新车辆数据
+            self.region_cars[region_id] = car_list
+            
+            # 检查装车区域
+            if load_region_id and region_id == load_region_id:
+                self.check_region_cars(region_name, car_list, 'load', current_time)
+            
+            # 检查卸车区域
+            if unload_region_id and region_id == unload_region_id:
+                self.check_region_cars(region_name, car_list, 'unload', current_time)
         
-        for row in range(self.table.rowCount()):
-            car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
-            addr = self.table.item(row, 4).text() if self.table.item(row, 4) else ''
+        self.update_region_status()
+    
+    def check_region_cars(self, region_name, car_list, task_type, current_time):
+        for car_id in car_list:
+            if not car_id:
+                continue
+
+            # 先初始化车牌
+            car_plate = None
+            car_state = None
+            # 根据carId找车牌和状态
+            for car in self.car_data:
+                if str(car.get('carId')) == str(car_id):
+                    car_plate = car.get('carPlate')
+                    car_state = car.get('stateCn', '')  # 获取状态
+                    break
             
-            if not car_plate or not addr:
+            # 找不到车牌直接跳过
+            if not car_plate:
+                continue
+                # 状态过滤:只处理 "在线-停车" 的车辆
+            if "在线-停车" not in car_state:
+                continue
+            # 核心:启用过滤(必须放这里)
+            if not self.car_checked_state.get(car_plate, False):
                 continue
             
-            # 初始化该车辆的触发记录
+            # 初始化记录
             if car_plate not in self.triggered_records:
                 self.triggered_records[car_plate] = {'load': None, 'unload': None}
-            
-            # 检查装车地址
-            if load_keyword and load_keyword in addr:
-                last_trigger = self.triggered_records[car_plate]['load']
-                if last_trigger is None or (current_time - last_trigger) >= timedelta(minutes=5):
-                    self.triggered_records[car_plate]['load'] = current_time
-                    self.add_log(f"🚛 {car_plate} 到达装车位置 ({load_keyword})")
-                    self.statusBar().showMessage(f"{car_plate} 到达装车位置")
-            
-            # 检查卸车地址
-            if unload_keyword and unload_keyword in addr:
-                last_trigger = self.triggered_records[car_plate]['unload']
-                if last_trigger is None or (current_time - last_trigger) >= timedelta(minutes=5):
-                    self.triggered_records[car_plate]['unload'] = current_time
-                    self.add_log(f"🚛 {car_plate} 到达卸车位置 ({unload_keyword})")
-                    self.statusBar().showMessage(f"{car_plate} 到达卸车位置")
-    
-    def add_log(self, message):
-        """添加日志"""
-        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-        log_line = f"[{timestamp}] {message}"
-        self.log_output.append(log_line)
+
+            last_trigger = self.triggered_records[car_plate][task_type]
+
+            if last_trigger is None or (current_time - last_trigger) >= timedelta(minutes=5):
+                self.triggered_records[car_plate][task_type] = current_time
+                task_name = "装车" if task_type == 'load' else "卸车"
+                self.add_log(f"🚛 {car_plate} 在 {region_name} 检测到{task_name}")
+                self.execute_task(car_plate, task_type)
+    
+    # ===== 执行任务 =====
+    def execute_task(self, car_plate, task_type):
+        """通过WebSocket执行任务"""
+        if not self.ws_client or not self.ws_client.is_running:
+            self.add_log("❌ WebSocket未连接,无法执行任务")
+            return
         
-        # 保持最新日志可见
-        scrollbar = self.log_output.verticalScrollBar()
-        scrollbar.setValue(scrollbar.maximum())
+        # 获取对应的客户端 userID
+        if task_type == 'load':
+            user_id = self.send_client_combo.currentData()
+        else:
+            user_id = self.receive_client_combo.currentData()
+        
+        if not user_id:
+            self.add_log(f"❌ 未选择{task_type}客户端")
+            return
+        
+        # 生成带车牌的文件路径: 文件夹路径/车牌.jpg
+        value = self.get_value_with_plate(car_plate)
+        
+        if not value:
+            self.add_log(f"⚠️ 未选择文件夹,发送空值,车牌: {car_plate}")
+        else:
+            self.add_log(f"📁 生成文件路径: {value}")
+        
+        # 发送指令
+        route = "发货" if task_type == 'load' else "收货"
+        success = self.ws_client.send(route, value, user_id)
+        
+        if success:
+            self.add_log(f"📤 发送{route}指令到 {user_id},车牌: {car_plate},value: {value}")
+        else:
+            self.add_log(f"❌ 发送{route}指令失败")
+    
+    # ===== 发送指令(手动) =====
+    def send_cmd_to_vehicle(self, car_plate, text_msg):
+        """发送指令到车辆(保留原有功能)"""
+        if not self.session_id or not self.username:
+            self.add_log("❌ 未登录,无法发送指令")
+            return
+        
+        params = {"textMsg": text_msg, "textType": 1, "signBit": "10010000"}
+        url = "http://119.91.58.175:9999/gps-web/api/send_cmd.jsp"
+        req_params = {
+            "userId": self.username,
+            "cmdId": "500",
+            "cmd": "CmdSendText",
+            "params": json.dumps(params, ensure_ascii=False),
+            "sessionId": self.session_id,
+            "plate": car_plate
+        }
+        
+        try:
+            response = requests.get(url, params=req_params, timeout=10)
+            response.encoding = 'utf-8'
+            self.add_log(f"📤 发送指令到 {car_plate}: {text_msg}")
+            if response.status_code == 200:
+                data = response.json()
+                code = data.get('rspCode')
+                if code == '0':
+                    self.add_log(f"✅ 指令发送成功")
+                else:
+                    self.add_log(f"⚠️ {data.get('rspDesc', '发送失败')}")
+            else:
+                self.add_log(f"❌ HTTP {response.status_code}")
+        except Exception as e:
+            self.add_log(f"❌ {str(e)}")
+    
+    # ===== 配置 =====
+    def save_config(self):
+        config = {
+            'username': self.username_input.text(),
+            'folder_path': self.folder_path_input.text()
+        }
+        try:
+            with open('config.json', 'w', encoding='utf-8') as f:
+                json.dump(config, f, indent=4, ensure_ascii=False)
+        except:
+            pass
+    
+    def load_config(self):
+        if os.path.exists('config.json'):
+            try:
+                with open('config.json', 'r', encoding='utf-8') as f:
+                    config = json.load(f)
+                    self.username_input.setText(config.get('username', '深金川'))
+                    folder_path = config.get('folder_path', '')
+                    if folder_path:
+                        self.folder_path_input.setText(folder_path)
+                        self.folder_path = folder_path
+            except:
+                pass
+    
+    # ===== 日志 =====
+    def add_log(self, message, level="INFO"):
+        """添加日志"""
+        timestamp = datetime.now().strftime("%H:%M:%S")
+        self.log_output.append(f"[{timestamp}] {message}")
+        self.log_output.verticalScrollBar().setValue(
+            self.log_output.verticalScrollBar().maximum()
+        )
+
+# ==================== 启动 ====================
 
 def main():
     app = QApplication(sys.argv)

+ 1063 - 0
控制vm虚拟机.py

@@ -0,0 +1,1063 @@
+import tkinter as tk
+from tkinter import ttk, messagebox
+import time
+import threading
+import random
+import win32gui
+import win32con
+import win32api
+import ctypes
+from ctypes import wintypes
+import platform
+import os
+import sys
+import json
+import websocket
+
+# ========== 幽灵键鼠加载 ==========
+def load_ghost_key_mouse():
+    """加载幽灵键鼠DLL"""
+    script_dir = os.path.dirname(os.path.abspath(__file__))
+    
+    # 根据系统架构选择DLL
+    if platform.architecture()[0] == "64bit":
+        dll_path = os.path.join(script_dir, "gbild64.dll")
+    else:
+        dll_path = os.path.join(script_dir, "gbild32.dll")
+    
+    if not os.path.exists(dll_path):
+        # 如果当前目录没有,尝试从exe所在目录加载
+        if getattr(sys, 'frozen', False):
+            script_dir = os.path.dirname(sys.executable)
+            if platform.architecture()[0] == "64bit":
+                dll_path = os.path.join(script_dir, "gbild64.dll")
+            else:
+                dll_path = os.path.join(script_dir, "gbild32.dll")
+    
+    try:
+        dll = ctypes.windll.LoadLibrary(dll_path)
+        print(f"幽灵键鼠DLL加载成功: {dll_path}")
+        return dll
+    except Exception as e:
+        print(f"幽灵键鼠DLL加载失败: {e}")
+        return None
+
+# 加载DLL
+ghost_dll = load_ghost_key_mouse()
+
+if ghost_dll:
+    # 设置接口返回值类型
+    ghost_dll.getmodel.restype = ctypes.c_char_p
+    ghost_dll.getserialnumber.restype = ctypes.c_char_p
+    ghost_dll.getproductiondate.restype = ctypes.c_char_p
+    ghost_dll.getfirmwareversion.restype = ctypes.c_char_p
+    ghost_dll.getclientscreenresolution.restype = ctypes.c_char_p
+    ghost_dll.readstring.restype = ctypes.c_char_p
+    ghost_dll.encryptstring.restype = ctypes.c_char_p
+    ghost_dll.decryptstring.restype = ctypes.c_char_p
+    ghost_dll.getproductname.restype = ctypes.c_char_p
+    ghost_dll.sdktype.restype = ctypes.c_char_p
+    ghost_dll.sdkversion.restype = ctypes.c_char_p
+
+    # ================ 设备操作 ================
+    def opendevice(index=0):
+        """打开设备(根据设备序号)"""
+        return ghost_dll.opendevice(index)
+
+    def closedevice():
+        """关闭设备"""
+        return ghost_dll.closedevice()
+
+    def isconnected():
+        """检查设备是否连接"""
+        return ghost_dll.isconnected()
+
+    # ================ 鼠标操作 ================
+    def movemouseto(x, y):
+        """移动鼠标到指定坐标"""
+        return ghost_dll.movemouseto(x, y)
+
+    def pressandreleasemousebutton(mbtn):
+        """按下并释放鼠标键 (1:左键, 2:右键, 3:中键)"""
+        return ghost_dll.pressandreleasemousebutton(mbtn)
+
+    def pressmousebutton(mbtn):
+        """按下鼠标键"""
+        return ghost_dll.pressmousebutton(mbtn)
+
+    def releasemousebutton(mbtn):
+        """释放鼠标键"""
+        return ghost_dll.releasemousebutton(mbtn)
+
+    def getmousex():
+        """获取鼠标当前X坐标"""
+        return ghost_dll.getmousex()
+
+    def getmousey():
+        """获取鼠标当前Y坐标"""
+        return ghost_dll.getmousey()
+
+    def setmousemovementdelay(maxd, mind):
+        """设置鼠标移动延时"""
+        return ghost_dll.setmousemovementdelay(maxd, mind)
+
+    def setmousemovementspeed(speedvalue):
+        """设置鼠标移动速度"""
+        return ghost_dll.setmousemovementspeed(speedvalue)
+
+    # ================ 键盘操作 ================
+    def presskeybyname(key_name):
+        """按下键(通过键名)"""
+        return ghost_dll.presskeybyname(key_name)
+
+    def releasekeybyname(key_name):
+        """释放键(通过键名)"""
+        return ghost_dll.releasekeybyname(key_name)
+
+    def combinationkey(key_sequence):
+        """组合键(如 b"ctrl+c")"""
+        return ghost_dll.combinationkey(key_sequence)
+
+    def pressandreleasekeybyname(key_name):
+        """按下并释放键"""
+        return ghost_dll.pressandreleasekeybyname(key_name)
+
+    def presskeybycode(key_code):
+        """按下键(通过键码)"""
+        return ghost_dll.presskeybycode(key_code)
+
+    def releasekeybycode(key_code):
+        """释放键(通过键码)"""
+        return ghost_dll.releasekeybycode(key_code)
+
+    def pressandreleasekeybycode(key_code):
+        """按下并释放键(通过键码)"""
+        return ghost_dll.pressandreleasekeybycode(key_code)
+
+    def clearkeys():
+        """清除所有按下的键"""
+        return ghost_dll.clearkeys()
+
+    # 尝试打开设备
+    device_id = opendevice(0)
+    if device_id == 0:
+        print("幽灵键鼠设备连接失败!")
+        ghost_available = False
+    else:
+        print("幽灵键鼠设备连接成功")
+        ghost_available = True
+else:
+    ghost_available = False
+    print("幽灵键鼠不可用,将使用系统API")
+
+
+class WebSocketClient:
+    """WebSocket客户端"""
+    def __init__(self, channel_type, message_callback, log_callback):
+        self.channel_type = channel_type
+        self.message_callback = message_callback
+        self.log_callback = log_callback
+        self.ws = None
+        self.is_connected = False
+        self.stop_flag = False
+        self.thread = None
+        self.user_id = None
+        
+    def connect(self):
+        """连接WebSocket服务器"""
+        def on_message(ws, message):
+            try:
+                data = json.loads(message)
+                msg_type = data.get("type")
+                
+                # 处理用户信息(登录成功)
+                if msg_type == "userInfo":
+                    self.is_connected = True
+                    self.user_id = data.get("id")
+                    if self.log_callback:
+                        self.log_callback(f"WS登录成功,用户ID: {self.user_id}")
+                    return
+                
+                # 处理其他消息
+                if self.message_callback:
+                    self.message_callback(data)
+                    
+            except Exception as e:
+                if self.log_callback:
+                    self.log_callback(f"WebSocket消息解析失败: {e}")
+        
+        def on_error(ws, error):
+            if self.log_callback:
+                self.log_callback(f"WebSocket错误: {error}")
+        
+        def on_close(ws, close_status_code, close_msg):
+            self.is_connected = False
+            if self.log_callback:
+                self.log_callback("WebSocket连接已关闭")
+            # 自动重连
+            if not self.stop_flag:
+                if self.log_callback:
+                    self.log_callback("3秒后尝试重新连接...")
+                time.sleep(3)
+                self.connect()
+        
+        def on_open(ws):
+            if self.log_callback:
+                self.log_callback(f"WebSocket连接成功,信道: {self.channel_type}")
+            # 发送登录信息
+            login_msg = {
+                "route": "login",
+                "type": self.channel_type,
+                "admin": True
+            }
+            ws.send(json.dumps(login_msg))
+        
+        try:
+            # 连接到本地服务器
+            self.ws = websocket.WebSocketApp(
+                'wss://ws.lamp.run',  # 你的服务器地址
+                on_open=on_open,
+                on_message=on_message,
+                on_error=on_error,
+                on_close=on_close
+            )
+            # 在新线程中运行
+            self.thread = threading.Thread(target=self.ws.run_forever, daemon=True)
+            self.thread.start()
+        except Exception as e:
+            if self.log_callback:
+                self.log_callback(f"WebSocket连接失败: {e}")
+    
+    def disconnect(self):
+        """断开WebSocket连接"""
+        self.stop_flag = True
+        if self.ws:
+            self.ws.close()
+        self.is_connected = False
+    
+    def send_message(self, route, value):
+        """发送消息 - 群发模式(不指定id)"""
+        if not self.ws or not self.is_connected:
+            if self.log_callback:
+                self.log_callback(f"⚠️ WS未连接,无法发送消息")
+            return False
+        
+        if not self.user_id:
+            if self.log_callback:
+                self.log_callback(f"⚠️ 未获取到用户ID,无法发送消息")
+            return False
+        
+        try:
+            # 构建消息格式 - 不包含id字段,实现群发
+            msg = {
+                "route": route,
+                "type": self.channel_type,
+                "userID": self.user_id,
+                "value": value
+            }
+            
+            # ===== 打印原始消息 =====
+            msg_json = json.dumps(msg)
+            if self.log_callback:
+                self.log_callback(f"📤 发送原始消息: {msg_json}")
+            
+            self.ws.send(msg_json)
+            if self.log_callback:
+                self.log_callback(f"📤 群发WS消息: route={route}, value={value}")
+            return True
+        except Exception as e:
+            if self.log_callback:
+                self.log_callback(f"发送消息失败: {e}")
+            return False
+
+
+class VmControlGUI:
+    def __init__(self, root):
+        self.root = root
+        self.root.title("虚拟机按键循环工具 - 双模式")
+        self.root.geometry("850x750")
+        self.root.resizable(True, True)
+
+        # ===== 脚本选择 =====
+        self.script_mode = tk.StringVar(value="A")
+        
+        # ===== 脚本A变量 =====
+        self.vm_window_title = tk.StringVar()
+        self.vm_windows = []
+        self.vm_hwnd = None
+        self.window_rect = None
+
+        # ===== 脚本A运行状态 =====
+        self.is_running = False
+        self.stop_flag = False
+        self.thread = None
+
+        # ===== 脚本A参数 =====
+        self.key_interval_min = tk.StringVar(value="300")
+        self.key_interval_max = tk.StringVar(value="800")
+        self.round_interval_min = tk.StringVar(value="3")
+        self.round_interval_max = tk.StringVar(value="5")
+        self.pause_interval_min = tk.StringVar(value="30")
+        self.pause_interval_max = tk.StringVar(value="50")
+        self.pause_duration = tk.StringVar(value="60")
+
+        # ===== 脚本B变量 =====
+        self.channel_type = tk.StringVar(value="test_channel")
+        self.ws_client = None
+        self.is_b_running = False
+        self.b_stop_flag = False
+        self.b_thread = None
+        
+        # 脚本B参数
+        self.b_key_interval_min = tk.StringVar(value="30")
+        self.b_key_interval_max = tk.StringVar(value="50")
+        self.b_stop_delay_min = tk.StringVar(value="10")
+        self.b_stop_delay_max = tk.StringVar(value="30")
+        self.b_wait_after_stop_min = tk.StringVar(value="200")
+        self.b_wait_after_stop_max = tk.StringVar(value="500")
+
+        # ===== 激活坐标 =====
+        self.ACTIVATE_X = 300
+        self.ACTIVATE_Y = 300
+
+        # ===== 鼠标移动延迟 =====
+        self.MOUSE_MOVE_DELAY = 0.1
+
+        # ===== 幽灵键鼠状态 =====
+        self.ghost_available = ghost_available
+        if self.ghost_available:
+            setmousemovementspeed(5)
+            setmousemovementdelay(10, 5)
+
+        self.create_widgets()
+        self.scan_vm_windows()
+        
+        # 显示幽灵键鼠状态
+        if self.ghost_available:
+            self.log("✅ 幽灵键鼠已连接")
+        else:
+            self.log("⚠️ 幽灵键鼠未连接,使用系统API")
+
+    def create_widgets(self):
+        main_frame = ttk.Frame(self.root, padding="10")
+        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
+
+        # ===== 脚本模式选择 =====
+        mode_frame = ttk.LabelFrame(main_frame, text="脚本模式", padding="10")
+        mode_frame.grid(row=0, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=5)
+        
+        ttk.Radiobutton(mode_frame, text="脚本A - 主控 (按键循环)", variable=self.script_mode, 
+                       value="A", command=self.on_mode_change).pack(side=tk.LEFT, padx=10)
+        ttk.Radiobutton(mode_frame, text="脚本B - 分机 (WS监听)", variable=self.script_mode, 
+                       value="B", command=self.on_mode_change).pack(side=tk.LEFT, padx=10)
+        
+        # 信道输入框
+        ttk.Label(mode_frame, text="信道:").pack(side=tk.LEFT, padx=(20, 5))
+        ttk.Entry(mode_frame, textvariable=self.channel_type, width=15).pack(side=tk.LEFT, padx=5)
+        
+        ttk.Button(mode_frame, text="连接WS", command=self.connect_websocket, width=8).pack(side=tk.LEFT, padx=5)
+        ttk.Button(mode_frame, text="断开WS", command=self.disconnect_websocket, width=8).pack(side=tk.LEFT, padx=5)
+        
+        # WS状态
+        self.ws_status_label = ttk.Label(mode_frame, text="WS: 未连接", foreground="gray")
+        self.ws_status_label.pack(side=tk.LEFT, padx=10)
+        
+        # 发送指令按钮(群发)
+        ttk.Button(mode_frame, text="📤 群发开始", command=self.send_start_command, width=10).pack(side=tk.LEFT, padx=5)
+        ttk.Button(mode_frame, text="📤 群发停止", command=self.send_stop_command, width=10).pack(side=tk.LEFT, padx=5)
+
+        # ===== 脚本A界面 =====
+        self.frame_a = ttk.Frame(main_frame)
+        self.frame_a.grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E, tk.N, tk.S))
+        
+        # 窗口搜索
+        ttk.Button(self.frame_a, text="🔍 搜索 VMware 窗口", command=self.scan_vm_windows, width=20).grid(
+            row=0, column=0, sticky=tk.W, pady=5
+        )
+        ttk.Label(self.frame_a, text="选择虚拟机窗口:").grid(row=0, column=1, sticky=tk.W, pady=5, padx=10)
+        self.vm_combo = ttk.Combobox(self.frame_a, textvariable=self.vm_window_title, width=45)
+        self.vm_combo.grid(row=0, column=2, sticky=(tk.W, tk.E), pady=5, padx=5)
+
+        self.window_info_label = ttk.Label(self.frame_a, text="窗口状态: 未选择", foreground="gray")
+        self.window_info_label.grid(row=1, column=0, columnspan=4, sticky=tk.W, pady=5)
+
+        # 脚本A参数
+        param_frame_a = ttk.LabelFrame(self.frame_a, text="脚本A参数设置", padding="10")
+        param_frame_a.grid(row=2, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=10)
+
+        ttk.Label(param_frame_a, text="按键间隔 (毫秒):").grid(row=0, column=0, sticky=tk.W, pady=3)
+        ttk.Label(param_frame_a, text="最小:").grid(row=0, column=1, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_a, textvariable=self.key_interval_min, width=8).grid(row=0, column=1, sticky=tk.W, pady=3, padx=(30, 0))
+        ttk.Label(param_frame_a, text="最大:").grid(row=0, column=2, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_a, textvariable=self.key_interval_max, width=8).grid(row=0, column=2, sticky=tk.W, pady=3, padx=(30, 0))
+
+        ttk.Label(param_frame_a, text="轮次间隔 (秒):").grid(row=1, column=0, sticky=tk.W, pady=3)
+        ttk.Label(param_frame_a, text="最小:").grid(row=1, column=1, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_a, textvariable=self.round_interval_min, width=8).grid(row=1, column=1, sticky=tk.W, pady=3, padx=(30, 0))
+        ttk.Label(param_frame_a, text="最大:").grid(row=1, column=2, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_a, textvariable=self.round_interval_max, width=8).grid(row=1, column=2, sticky=tk.W, pady=3, padx=(30, 0))
+
+        ttk.Label(param_frame_a, text="暂停间隔 (分钟):").grid(row=2, column=0, sticky=tk.W, pady=3)
+        ttk.Label(param_frame_a, text="最小:").grid(row=2, column=1, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_a, textvariable=self.pause_interval_min, width=8).grid(row=2, column=1, sticky=tk.W, pady=3, padx=(30, 0))
+        ttk.Label(param_frame_a, text="最大:").grid(row=2, column=2, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_a, textvariable=self.pause_interval_max, width=8).grid(row=2, column=2, sticky=tk.W, pady=3, padx=(30, 0))
+        ttk.Label(param_frame_a, text="暂停时长(秒):").grid(row=2, column=3, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_a, textvariable=self.pause_duration, width=8).grid(row=2, column=3, sticky=tk.W, pady=3, padx=(5, 0))
+
+        # 脚本A按钮
+        btn_frame_a = ttk.Frame(self.frame_a)
+        btn_frame_a.grid(row=3, column=0, columnspan=4, pady=10)
+
+        self.start_btn = ttk.Button(btn_frame_a, text="▶ 开始循环", command=self.start_loop, width=15)
+        self.start_btn.pack(side=tk.LEFT, padx=5)
+
+        self.stop_btn = ttk.Button(btn_frame_a, text="⏹ 停止循环", command=self.stop_loop, width=15, state=tk.DISABLED)
+        self.stop_btn.pack(side=tk.LEFT, padx=5)
+
+        ttk.Button(btn_frame_a, text="测试点击", command=self.test_click, width=15).pack(side=tk.LEFT, padx=5)
+        ttk.Button(btn_frame_a, text="测试按键", command=self.test_keys, width=15).pack(side=tk.LEFT, padx=5)
+
+        # ===== 脚本B界面 =====
+        self.frame_b = ttk.Frame(main_frame)
+        self.frame_b.grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E, tk.N, tk.S))
+        self.frame_b.grid_remove()
+
+        # 脚本B参数
+        param_frame_b = ttk.LabelFrame(self.frame_b, text="脚本B参数设置", padding="10")
+        param_frame_b.grid(row=0, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=10)
+
+        ttk.Label(param_frame_b, text="按键间隔 (秒):").grid(row=0, column=0, sticky=tk.W, pady=3)
+        ttk.Label(param_frame_b, text="最小:").grid(row=0, column=1, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_b, textvariable=self.b_key_interval_min, width=8).grid(row=0, column=1, sticky=tk.W, pady=3, padx=(30, 0))
+        ttk.Label(param_frame_b, text="最大:").grid(row=0, column=2, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_b, textvariable=self.b_key_interval_max, width=8).grid(row=0, column=2, sticky=tk.W, pady=3, padx=(30, 0))
+
+        ttk.Label(param_frame_b, text="停止延迟 (秒):").grid(row=1, column=0, sticky=tk.W, pady=3)
+        ttk.Label(param_frame_b, text="最小:").grid(row=1, column=1, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_b, textvariable=self.b_stop_delay_min, width=8).grid(row=1, column=1, sticky=tk.W, pady=3, padx=(30, 0))
+        ttk.Label(param_frame_b, text="最大:").grid(row=1, column=2, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_b, textvariable=self.b_stop_delay_max, width=8).grid(row=1, column=2, sticky=tk.W, pady=3, padx=(30, 0))
+
+        ttk.Label(param_frame_b, text="停止后延迟 (毫秒):").grid(row=2, column=0, sticky=tk.W, pady=3)
+        ttk.Label(param_frame_b, text="最小:").grid(row=2, column=1, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_b, textvariable=self.b_wait_after_stop_min, width=8).grid(row=2, column=1, sticky=tk.W, pady=3, padx=(30, 0))
+        ttk.Label(param_frame_b, text="最大:").grid(row=2, column=2, sticky=tk.W, pady=3, padx=(10, 0))
+        ttk.Entry(param_frame_b, textvariable=self.b_wait_after_stop_max, width=8).grid(row=2, column=2, sticky=tk.W, pady=3, padx=(30, 0))
+
+        # 脚本B状态
+        self.b_status_label = ttk.Label(self.frame_b, text="脚本B状态: 等待启动信号", foreground="gray")
+        self.b_status_label.grid(row=1, column=0, columnspan=4, sticky=tk.W, pady=5)
+
+        # ===== 公共区域 =====
+        self.status_label = ttk.Label(main_frame, text="状态: 停止", foreground="gray")
+        self.status_label.grid(row=2, column=0, columnspan=4, sticky=tk.W, pady=5)
+
+        ttk.Label(main_frame, text="执行日志:").grid(row=3, column=0, sticky=tk.W, pady=5)
+
+        self.log_text = tk.Text(main_frame, height=14, width=100, font=("Consolas", 9))
+        self.log_text.grid(row=4, column=0, columnspan=4, pady=5, sticky=(tk.W, tk.E, tk.N, tk.S))
+
+        scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.log_text.yview)
+        scrollbar.grid(row=4, column=4, sticky=(tk.N, tk.S))
+        self.log_text.config(yscrollcommand=scrollbar.set)
+
+        ttk.Button(main_frame, text="清空日志", command=self.clear_log, width=15).grid(row=5, column=0, columnspan=4, pady=5)
+
+        main_frame.columnconfigure(2, weight=1)
+        main_frame.rowconfigure(4, weight=1)
+        self.root.columnconfigure(0, weight=1)
+        self.root.rowconfigure(0, weight=1)
+
+    def on_mode_change(self):
+        """切换脚本模式"""
+        mode = self.script_mode.get()
+        if mode == "A":
+            self.frame_a.grid()
+            self.frame_b.grid_remove()
+        else:
+            self.frame_a.grid_remove()
+            self.frame_b.grid()
+
+    def log(self, message):
+        import datetime
+        timestamp = datetime.datetime.now().strftime("%H:%M:%S")
+        self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
+        self.log_text.see(tk.END)
+        self.root.update()
+
+    def clear_log(self):
+        self.log_text.delete(1.0, tk.END)
+
+    # ========== WebSocket相关 ==========
+    def connect_websocket(self):
+        """连接WebSocket"""
+        channel = self.channel_type.get().strip()
+        if not channel:
+            messagebox.showerror("错误", "请输入信道名称!")
+            return
+        
+        if self.ws_client and self.ws_client.is_connected:
+            self.log("WebSocket已连接")
+            return
+        
+        self.ws_client = WebSocketClient(
+            channel_type=channel,
+            message_callback=self.on_ws_message,
+            log_callback=self.log
+        )
+        self.ws_client.connect()
+        self.ws_status_label.config(text="WS: 连接中...", foreground="orange")
+    
+    def disconnect_websocket(self):
+        """断开WebSocket"""
+        if self.ws_client:
+            self.ws_client.disconnect()
+            self.ws_client = None
+            self.ws_status_label.config(text="WS: 已断开", foreground="gray")
+            self.log("WebSocket已断开")
+    
+    def send_start_command(self):
+        """群发开始指令"""
+        if not self.ws_client or not self.ws_client.is_connected:
+            self.log("⚠️ WS未连接,无法发送指令")
+            messagebox.showerror("错误", "请先连接WebSocket!")
+            return
+        
+        # 群发 - 不指定id
+        self.ws_client.send_message("start", "开始执行")
+        self.log("📤 已群发开始指令")
+    
+    def send_stop_command(self):
+        """群发停止指令"""
+        if not self.ws_client or not self.ws_client.is_connected:
+            self.log("⚠️ WS未连接,无法发送指令")
+            messagebox.showerror("错误", "请先连接WebSocket!")
+            return
+        
+        # 群发 - 不指定id
+        self.ws_client.send_message("stop", "停止执行")
+        self.log("📤 已群发停止指令")
+    
+    def on_ws_message(self, data):
+        """处理WebSocket消息"""
+        try:
+            # ===== 打印收到的原始消息 =====
+            self.log(f"📨 收到原始消息: {json.dumps(data)}")
+            
+            msg_type = data.get("type")
+            value = data.get("value")
+            user_id = data.get("userID")
+            
+            self.log(f"📨 解析结果: type={msg_type}, value={value}, from={user_id}")
+            
+            if msg_type == "start":
+                self.log(f"📨 收到开始指令")
+                # 启动脚本B
+                self.start_script_b()
+            elif msg_type == "stop":
+                self.log(f"📨 收到停止指令")
+                # 停止脚本B
+                self.stop_script_b()
+            elif msg_type == "close":
+                self.log(f"📨 收到关闭指令,可能是ID冲突")
+            else:
+                self.log(f"收到其他消息: {msg_type}")
+                
+        except Exception as e:
+            self.log(f"处理WS消息失败: {e}")
+
+    # ========== 脚本B相关 ==========
+    def start_script_b(self):
+        """启动脚本B"""
+        if self.is_b_running:
+            self.log("脚本B已在运行中")
+            return
+        
+        self.b_stop_flag = False
+        self.is_b_running = True
+        self.b_status_label.config(text="脚本B状态: 运行中", foreground="green")
+        
+        self.b_thread = threading.Thread(target=self.script_b_worker, daemon=True)
+        self.b_thread.start()
+        self.log("🚀 脚本B已启动")
+
+    def stop_script_b(self):
+        """停止脚本B"""
+        if not self.is_b_running:
+            self.log("脚本B未运行")
+            return
+        
+        self.b_stop_flag = True
+        self.b_status_label.config(text="脚本B状态: 正在停止...", foreground="orange")
+        self.log("⏹ 脚本B正在停止...")
+
+    def script_b_worker(self):
+        """脚本B工作线程 - 大键盘单键循环"""
+        try:
+            # 按键间隔
+            interval_min = int(self.b_key_interval_min.get())
+            interval_max = int(self.b_key_interval_max.get())
+            
+            # 停止延迟
+            stop_delay_min = int(self.b_stop_delay_min.get())
+            stop_delay_max = int(self.b_stop_delay_max.get())
+            
+            # 停止后延迟(毫秒)
+            wait_min = int(self.b_wait_after_stop_min.get()) / 1000.0
+            wait_max = int(self.b_wait_after_stop_max.get()) / 1000.0
+            
+            self.log("=" * 50)
+            self.log("🚀 脚本B - 大键盘单键循环开始")
+            self.log(f"按键: 小键盘2")
+            self.log(f"按键间隔: {interval_min}-{interval_max}秒")
+            self.log("=" * 50)
+            
+            while not self.b_stop_flag:
+                # 按小键盘2
+                self.log("发送: 小键盘2")
+                self.press_key(0x62, True)
+                
+                # 等待间隔
+                interval = random.uniform(interval_min, interval_max)
+                sleep_chunks = max(1, int(interval / 0.5))
+                for _ in range(sleep_chunks):
+                    if self.b_stop_flag:
+                        break
+                    time.sleep(0.5)
+            
+            # ===== 执行停止序列 =====
+            self.log("脚本B停止序列开始")
+            
+            # 随机延迟
+            stop_delay = random.uniform(stop_delay_min, stop_delay_max)
+            self.log(f"停止前延迟: {stop_delay:.1f}秒")
+            time.sleep(stop_delay)
+            
+            # 执行3次停止序列
+            for i in range(3):
+                if self.b_stop_flag:
+                    break
+                self.log(f"停止序列 {i+1}/3")
+                
+                # 按 = 
+                wait_time = random.uniform(wait_min, wait_max)
+                self.log(f"  按 = (延迟 {wait_time*1000:.0f}ms)")
+                self.press_key(0xBB, False)
+                time.sleep(wait_time)
+                
+                # 按小键盘2
+                wait_time = random.uniform(wait_min, wait_max)
+                self.log(f"  按 小键盘2 (延迟 {wait_time*1000:.0f}ms)")
+                self.press_key(0x62, True)
+                time.sleep(wait_time)
+                
+                # 按 ESC
+                self.log(f"  按 ESC")
+                self.press_key(0x1B, False)
+                time.sleep(0.2)
+            
+            self.log("✅ 脚本B停止序列完成")
+            self.b_status_label.config(text="脚本B状态: 已停止", foreground="gray")
+            
+        except Exception as e:
+            self.log(f"脚本B出错: {e}")
+        finally:
+            self.is_b_running = False
+            self.b_status_label.config(text="脚本B状态: 已停止", foreground="gray")
+            self.log("脚本B已结束")
+
+    # ========== 窗口扫描 ==========
+    def scan_vm_windows(self):
+        self.log("正在扫描 VMware 窗口...")
+        self.vm_windows = []
+
+        def enum_callback(hwnd, _):
+            if win32gui.IsWindowVisible(hwnd):
+                title = win32gui.GetWindowText(hwnd)
+                if "VMware Workstation" in title and title.strip():
+                    self.vm_windows.append((title, hwnd))
+            return True
+
+        win32gui.EnumWindows(enum_callback, None)
+
+        if self.vm_windows:
+            titles = [f"{i+1}. {t[0]}" for i, t in enumerate(self.vm_windows)]
+            self.vm_combo["values"] = titles
+            self.vm_combo.current(0)
+            self.vm_window_title.set(titles[0])
+            self.log(f"找到 {len(self.vm_windows)} 个 VMware 窗口")
+            self.window_info_label.config(text=f"找到 {len(self.vm_windows)} 个窗口", foreground="green")
+            self.select_window(0)
+        else:
+            self.log("未找到 VMware 窗口")
+            self.window_info_label.config(text="未找到 VMware 窗口", foreground="red")
+            self.vm_combo["values"] = []
+
+        self.vm_combo.bind("<<ComboboxSelected>>", self.on_window_selected)
+
+    def on_window_selected(self, event=None):
+        selection = self.vm_combo.current()
+        if selection >= 0:
+            self.select_window(selection)
+
+    def select_window(self, index):
+        if index < len(self.vm_windows):
+            title, hwnd = self.vm_windows[index]
+            self.vm_hwnd = hwnd
+            self.vm_window_title.set(f"{index+1}. {title}")
+            
+            try:
+                rect = win32gui.GetWindowRect(hwnd)
+                self.window_rect = rect
+                self.window_info_label.config(
+                    text=f"已选择: {title} (大小: {rect[2]-rect[0]}x{rect[3]-rect[1]})",
+                    foreground="blue"
+                )
+                self.log(f"已选择窗口: {title}")
+            except Exception as e:
+                self.log(f"获取窗口信息失败: {e}")
+
+    # ========== 坐标转换 ==========
+    def get_absolute_coords(self, x, y):
+        if self.window_rect is None:
+            return None, None
+
+        left, top, right, bottom = self.window_rect
+        offset_x = 8
+        offset_y = 30
+        return left + offset_x + x, top + offset_y + y
+
+    def activate_window(self):
+        if self.vm_hwnd is None:
+            return False
+
+        try:
+            if win32gui.IsIconic(self.vm_hwnd):
+                win32gui.ShowWindow(self.vm_hwnd, win32con.SW_RESTORE)
+            
+            win32gui.SetForegroundWindow(self.vm_hwnd)
+            win32gui.BringWindowToTop(self.vm_hwnd)
+            
+            for _ in range(5):
+                if win32gui.GetForegroundWindow() == self.vm_hwnd:
+                    break
+                time.sleep(0.1)
+                win32gui.SetForegroundWindow(self.vm_hwnd)
+            
+            rect = win32gui.GetWindowRect(self.vm_hwnd)
+            self.window_rect = rect
+            return True
+        except Exception as e:
+            self.log(f"激活窗口失败: {e}")
+            return False
+
+    def click_at(self, x, y, button="left"):
+        """使用幽灵键鼠在指定屏幕坐标点击"""
+        try:
+            if self.ghost_available:
+                ret = movemouseto(x, y)
+                if ret != 1:
+                    self.log(f"幽灵键鼠移动失败: {ret}")
+                    return False
+                
+                time.sleep(0.05)
+                
+                if button == "left":
+                    ret = pressandreleasemousebutton(1)
+                else:
+                    ret = pressandreleasemousebutton(2)
+                
+                if ret != 1:
+                    self.log(f"幽灵键鼠点击失败: {ret}")
+                    return False
+                return True
+            else:
+                win32api.SetCursorPos((x, y))
+                time.sleep(0.05)
+                
+                if button == "left":
+                    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0)
+                    time.sleep(0.05)
+                    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0)
+                else:
+                    win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTDOWN, x, y, 0, 0)
+                    time.sleep(0.05)
+                    win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTUP, x, y, 0, 0)
+                return True
+        except Exception as e:
+            self.log(f"点击失败: {e}")
+            return False
+
+    def press_key(self, key_code, extended=False):
+        """使用幽灵键鼠模拟按键"""
+        try:
+            # 按键名称映射
+            key_name_map = {
+                0x68: b"8",
+                0x62: b"2",
+                0x25: b"left",
+                0x20: b"space",
+                0x26: b"up",
+                0x28: b"down",
+                0x27: b"right",
+                0x1B: b"esc",
+                0xBB: b"=",
+            }
+            
+            if self.ghost_available:
+                if key_code in key_name_map:
+                    ret = pressandreleasekeybyname(key_name_map[key_code])
+                    if ret != 1:
+                        ret = pressandreleasekeybycode(key_code)
+                    return ret == 1
+                else:
+                    ret = pressandreleasekeybycode(key_code)
+                    return ret == 1
+            else:
+                if extended:
+                    win32api.keybd_event(key_code, 0, win32con.KEYEVENTF_EXTENDEDKEY, 0)
+                    time.sleep(0.03)
+                    win32api.keybd_event(key_code, 0, win32con.KEYEVENTF_EXTENDEDKEY | win32con.KEYEVENTF_KEYUP, 0)
+                else:
+                    win32api.keybd_event(key_code, 0, 0, 0)
+                    time.sleep(0.03)
+                    win32api.keybd_event(key_code, 0, win32con.KEYEVENTF_KEYUP, 0)
+                return True
+        except Exception as e:
+            self.log(f"按键失败: {e}")
+            return False
+
+    # ========== 脚本A函数 ==========
+    def test_click(self):
+        self.log("=" * 50)
+        self.log("测试点击 (300, 300)...")
+
+        if self.vm_hwnd is None:
+            self.log("错误: 未选择窗口")
+            return
+
+        if not self.activate_window():
+            self.log("激活窗口失败")
+            return
+
+        abs_x, abs_y = self.get_absolute_coords(self.ACTIVATE_X, self.ACTIVATE_Y)
+        if abs_x is None:
+            return
+
+        self.log(f"屏幕坐标: ({abs_x}, {abs_y})")
+        self.log(f"使用: {'幽灵键鼠' if self.ghost_available else '系统API'}")
+
+        if self.click_at(abs_x, abs_y):
+            self.log("点击成功!")
+        else:
+            self.log("点击失败")
+
+        self.log("=" * 50)
+
+    def test_keys(self):
+        self.log("=" * 50)
+        self.log("测试按键序列")
+
+        if self.vm_hwnd is None:
+            self.log("错误: 未选择窗口")
+            return
+
+        if not self.activate_window():
+            self.log("激活窗口失败")
+            return
+
+        abs_x, abs_y = self.get_absolute_coords(self.ACTIVATE_X, self.ACTIVATE_Y)
+        if abs_x is None:
+            return
+        
+        self.log("点击激活虚拟机...")
+        self.click_at(abs_x, abs_y)
+        time.sleep(0.5)
+
+        keys = [
+            (0x68, True, "小键盘8"),
+            (0x62, True, "小键盘2"),
+            (0x25, False, "左键"),
+            (0x20, False, "空格"),
+        ]
+
+        self.log(f"使用: {'幽灵键鼠' if self.ghost_available else '系统API'}")
+
+        for key_code, extended, name in keys:
+            self.log(f"发送: {name}")
+            self.press_key(key_code, extended)
+            time.sleep(0.3)
+
+        self.log("测试完成")
+        self.log("=" * 50)
+
+    def loop_worker(self):
+        keys = [
+            (0x68, True, "小键盘8"),
+            (0x62, True, "小键盘2"),
+            (0x25, False, "左键"),
+            (0x20, False, "空格"),
+        ]
+
+        key_min = int(self.key_interval_min.get()) / 1000.0
+        key_max = int(self.key_interval_max.get()) / 1000.0
+        round_min = int(self.round_interval_min.get())
+        round_max = int(self.round_interval_max.get())
+        pause_min = int(self.pause_interval_min.get()) * 60
+        pause_max = int(self.pause_interval_max.get()) * 60
+        pause_dur = int(self.pause_duration.get())
+
+        round_count = 0
+        total_keys = 0
+        last_pause_time = time.time()
+
+        self.log("=" * 50)
+        self.log("🚀 脚本A循环开始!")
+        self.log(f"使用: {'幽灵键鼠' if self.ghost_available else '系统API'}")
+        self.log(f"按键: 小键盘8 -> 小键盘2 -> 左键 -> 空格")
+        self.log("=" * 50)
+
+        while not self.stop_flag:
+            try:
+                if not win32gui.IsWindow(self.vm_hwnd):
+                    self.log("窗口已关闭,停止循环")
+                    break
+
+                if not self.activate_window():
+                    self.log("无法激活窗口,等待 3 秒...")
+                    time.sleep(3)
+                    continue
+
+                abs_x, abs_y = self.get_absolute_coords(self.ACTIVATE_X, self.ACTIVATE_Y)
+                if abs_x is None:
+                    time.sleep(2)
+                    continue
+                
+                self.click_at(abs_x, abs_y)
+                time.sleep(0.3)
+
+                for key_code, extended, key_name in keys:
+                    if self.stop_flag:
+                        break
+                    
+                    self.log(f"发送: {key_name}")
+                    self.press_key(key_code, extended)
+                    total_keys += 1
+                    
+                    interval = random.uniform(key_min, key_max)
+                    time.sleep(interval)
+
+                round_count += 1
+
+                if round_count % 10 == 0:
+                    self.log(f"📊 已执行 {round_count} 轮")
+
+                current_time = time.time()
+                if current_time - last_pause_time >= random.uniform(pause_min, pause_max):
+                    self.log(f"⏸ 暂停 {pause_dur} 秒")
+                    self.status_label.config(text=f"状态: 暂停中 ({pause_dur}s)", foreground="orange")
+                    
+                    for _ in range(int(pause_dur / 0.5)):
+                        if self.stop_flag:
+                            break
+                        time.sleep(0.5)
+                    
+                    self.log("▶ 暂停结束")
+                    self.status_label.config(text="状态: 运行中", foreground="green")
+                    last_pause_time = time.time()
+                    continue
+
+                round_interval = random.uniform(round_min, round_max)
+                sleep_chunks = max(1, int(round_interval / 0.5))
+                for _ in range(sleep_chunks):
+                    if self.stop_flag:
+                        break
+                    time.sleep(0.5)
+
+            except Exception as e:
+                self.log(f"循环出错: {e}")
+                time.sleep(2)
+
+        self.log(f"🏁 脚本A结束!共执行 {round_count} 轮")
+        self.status_label.config(text="状态: 已停止", foreground="gray")
+        self.root.after(0, self.on_loop_stopped)
+
+    def start_loop(self):
+        if self.vm_hwnd is None:
+            messagebox.showerror("错误", "请先选择虚拟机窗口!")
+            return
+
+        if self.is_running:
+            return
+
+        if not self.ghost_available:
+            if not messagebox.askyesno("提示", "幽灵键鼠未连接,将使用系统API模拟按键。\n继续吗?"):
+                return
+
+        # ===== 新增:通过WebSocket通知同信道的脚本B =====
+        if self.ws_client and self.ws_client.is_connected:
+            self.ws_client.send_message("start", "开始执行")
+            self.log("📤 已群发开始指令到同信道脚本B")
+        else:
+            self.log("⚠️ WebSocket未连接,无法通知脚本B")
+
+        self.is_running = True
+        self.stop_flag = False
+        self.status_label.config(text="状态: 运行中", foreground="green")
+        self.start_btn.config(state=tk.DISABLED)
+        self.stop_btn.config(state=tk.NORMAL)
+
+        self.thread = threading.Thread(target=self.loop_worker, daemon=True)
+        self.thread.start()
+
+    def stop_loop(self):
+        self.log("⏹ 正在停止脚本A...")
+        
+        # ===== 新增:通过WebSocket通知同信道的脚本B停止 =====
+        if self.ws_client and self.ws_client.is_connected:
+            self.ws_client.send_message("stop", "停止执行")
+            self.log("📤 已群发停止指令到同信道脚本B")
+        else:
+            self.log("⚠️ WebSocket未连接,无法通知脚本B")
+        
+        self.stop_flag = True
+        self.status_label.config(text="状态: 正在停止...", foreground="orange")
+        self.start_btn.config(state=tk.NORMAL)
+        self.stop_btn.config(state=tk.DISABLED)
+
+    def on_loop_stopped(self):
+        self.is_running = False
+        self.start_btn.config(state=tk.NORMAL)
+        self.stop_btn.config(state=tk.DISABLED)
+        self.status_label.config(text="状态: 已停止", foreground="gray")
+
+    def __del__(self):
+        if self.ws_client:
+            self.ws_client.disconnect()
+        if self.ghost_available:
+            try:
+                closedevice()
+            except:
+                pass
+
+
+if __name__ == "__main__":
+    # 检查依赖
+    if not ghost_available:
+        try:
+            import win32api
+        except ImportError:
+            print("需要安装 pywin32: pip install pywin32")
+            sys.exit(1)
+    
+    # 检查websocket-client
+    try:
+        import websocket
+    except ImportError:
+        print("需要安装 websocket-client: pip install websocket-client")
+        sys.exit(1)
+    
+    root = tk.Tk()
+    app = VmControlGUI(root)
+    root.mainloop()

+ 0 - 38
米画师.spec

@@ -1,38 +0,0 @@
-# -*- mode: python ; coding: utf-8 -*-
-
-
-a = Analysis(
-    ['米画师.py'],
-    pathex=[],
-    binaries=[],
-    datas=[],
-    hiddenimports=[],
-    hookspath=[],
-    hooksconfig={},
-    runtime_hooks=[],
-    excludes=[],
-    noarchive=False,
-    optimize=0,
-)
-pyz = PYZ(a.pure)
-
-exe = EXE(
-    pyz,
-    a.scripts,
-    a.binaries,
-    a.datas,
-    [],
-    name='米画师',
-    debug=False,
-    bootloader_ignore_signals=False,
-    strip=False,
-    upx=True,
-    upx_exclude=[],
-    runtime_tmpdir=None,
-    console=False,
-    disable_windowed_traceback=False,
-    argv_emulation=False,
-    target_arch=None,
-    codesign_identity=None,
-    entitlements_file=None,
-)

+ 568 - 151
飞书提交数据.py

@@ -9,11 +9,18 @@ import random
 import re
 from datetime import datetime
 import os
+import shutil
+from openpyxl import load_workbook
+from openpyxl.utils import get_column_letter
+from PIL import Image
+import io
+import base64
+import zlib
 
 class FeishuSubmitApp:
     def __init__(self, root):
         self.root = root
-        self.root.title("飞书表格数据提交工具")
+        self.root.title("飞书表格数据提交工具 - 自动提取图片")
         self.root.geometry("1100x800")
         
         # 状态变量
@@ -22,16 +29,20 @@ class FeishuSubmitApp:
         self.excel_file = ""
         self.cookie_string = ""
         
+        # 图片缓存
+        self.image_cache = {}  # {行号: [图片数据列表]}
+        self.image_extract_folder = None
+        self.uploaded_cache = {}  # 缓存已上传的图片token {行号: attachment_info}
+        
         # 从curl提取的默认配置
         self.default_headers = {
             "accept": "application/json, text/plain, */*",
             "accept-language": "zh-CN,zh;q=0.9",
             "content-type": "application/json",
-            "f-version": "docs-6-9-1780996889854",
+            "f-version": "docs-6-17-1781628518399",
             "origin": "https://xhd.feishu.cn",
             "priority": "u=1, i",
             "referer": "https://xhd.feishu.cn/share/base/form/shrcnJqFUFF0sqlmrfsWpDNf8df",
-            "request-id": "HgNmkEtK9iNf-7646228072081099729",
             "sec-ch-ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
             "sec-ch-ua-mobile": "?0",
             "sec-ch-ua-platform": '"Windows"',
@@ -40,10 +51,8 @@ class FeishuSubmitApp:
             "sec-fetch-site": "same-origin",
             "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
             "x-csrftoken": "83aae12269bced10680d3ccf817f82262305e7d0-1781579108",
-            "x-request-id": "HgNmkEtK9iNf-7646228072081099729",
-            "x-tt-logid": "02178175254676300000000000000000000ffffe33263555f5be8",
-            "x-tt-trace": "1",
-            "x-tt-trace-id": "HgNmkEtK9iNf-7646228072081099729"
+            "x-tt-logid": "02178275091573600000000000000000000ffff5bd640caae0cb8",
+            "x-tt-trace": "1"
         }
         
         self.url = "https://xhd.feishu.cn/space/api/bitable/share/content"
@@ -92,11 +101,11 @@ class FeishuSubmitApp:
         
         # 间隔配置
         ttk.Label(control_frame, text="最小间隔(秒):").grid(row=0, column=0, sticky=tk.W)
-        self.min_interval_var = tk.StringVar(value="2")
+        self.min_interval_var = tk.StringVar(value="20")
         ttk.Entry(control_frame, textvariable=self.min_interval_var, width=10).grid(row=0, column=1, padx=(5, 15), sticky=tk.W)
         
         ttk.Label(control_frame, text="最大间隔(秒):").grid(row=0, column=2, sticky=tk.W)
-        self.max_interval_var = tk.StringVar(value="5")
+        self.max_interval_var = tk.StringVar(value="50")
         ttk.Entry(control_frame, textvariable=self.max_interval_var, width=10).grid(row=0, column=3, padx=(5, 15), sticky=tk.W)
         
         ttk.Label(control_frame, text="起始行:").grid(row=0, column=4, sticky=tk.W)
@@ -114,7 +123,8 @@ class FeishuSubmitApp:
         self.stop_btn = ttk.Button(btn_frame, text="⏹ 停止", command=self.stop_send, state=tk.DISABLED)
         self.stop_btn.pack(side=tk.LEFT, padx=(0, 10))
         
-        ttk.Button(btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT)
+        ttk.Button(btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT, padx=(0, 10))
+        ttk.Button(btn_frame, text="📸 提取图片", command=self.extract_images).pack(side=tk.LEFT)
         
         # 状态显示
         self.status_var = tk.StringVar(value="就绪")
@@ -164,9 +174,7 @@ class FeishuSubmitApp:
         """从剪贴板粘贴Cookie"""
         try:
             clipboard_text = self.root.clipboard_get()
-            # 尝试从curl命令中提取cookie
             if "cookie:" in clipboard_text.lower() or "-H" in clipboard_text:
-                # 尝试从curl -b 或 -H "cookie: ..." 提取
                 cookie_match = re.search(r'-b\s+"([^"]+)"', clipboard_text)
                 if not cookie_match:
                     cookie_match = re.search(r'-H\s+"cookie:\s*([^"]+)"', clipboard_text, re.IGNORECASE)
@@ -176,7 +184,6 @@ class FeishuSubmitApp:
                     self.log("已从剪贴板提取Cookie", "success")
                     return
             
-            # 直接粘贴
             self.cookie_text.delete(1.0, tk.END)
             self.cookie_text.insert(1.0, clipboard_text)
             self.log("已粘贴Cookie", "info")
@@ -239,77 +246,341 @@ class FeishuSubmitApp:
                 cookies[key.strip()] = value.strip()
         return cookies
     
-    def extract_image_info(self, row):
+    def _get_csrf_token(self):
+        """从cookie中提取csrf_token"""
+        cookie_text = self.cookie_text.get(1.0, tk.END).strip()
+        # 匹配 _csrf_token=xxx 或 csrf_token=xxx
+        match = re.search(r'_csrf_token=([^;]+)', cookie_text)
+        if match:
+            return match.group(1)
+        # 也尝试匹配 x-csrftoken
+        match = re.search(r'x-csrftoken[=:]\s*([^;\s]+)', cookie_text, re.IGNORECASE)
+        if match:
+            return match.group(1)
+        # 返回默认值
+        return "83aae12269bced10680d3ccf817f82262305e7d0-1781579108"
+    
+    def _get_mime_type(self, filename):
+        """获取文件MIME类型"""
+        ext = os.path.splitext(filename)[1].lower()
+        mime_map = {
+            '.jpg': 'image/jpeg',
+            '.jpeg': 'image/jpeg',
+            '.png': 'image/png',
+            '.gif': 'image/gif',
+            '.bmp': 'image/bmp',
+            '.webp': 'image/webp'
+        }
+        return mime_map.get(ext, 'image/jpeg')
+    
+    def upload_image(self, image_data, filename="image.jpg"):
         """
-        从行数据中提取图片信息
-        返回: dict 包含图片字段名称和值
+        完整上传图片到飞书,返回file_token信息
         """
-        image_fields = {}
-        
-        # 常见的图片字段名(根据你的实际情况调整)
-        possible_image_columns = ['图片', '照片', '附件', '头像', 'image', 'photo', 'picture', 'img']
-        
-        # 检查所有列,寻找可能包含图片的列
-        for col in row.index:
-            col_lower = str(col).lower()
-            # 检查列名是否包含图片相关关键词
-            if any(keyword in col_lower for keyword in ['图', '片', '照', '像', 'image', 'photo', 'pic', 'img', '附件']):
-                value = row.get(col)
-                if pd.notna(value) and value:
-                    # 检查是否为文件路径或URL
-                    value_str = str(value).strip()
-                    if value_str:
-                        # 检查是否为文件路径
-                        if os.path.exists(value_str):
-                            image_fields[col] = {
-                                'type': 'file',
-                                'path': value_str,
-                                'size': os.path.getsize(value_str)
-                            }
-                            self.log(f"  📎 检测到图片文件: {col} -> {os.path.basename(value_str)} ({os.path.getsize(value_str)} bytes)", "debug")
-                        # 检查是否为URL
-                        elif value_str.startswith(('http://', 'https://')):
-                            image_fields[col] = {
-                                'type': 'url',
-                                'url': value_str
-                            }
-                            self.log(f"  🔗 检测到图片URL: {col} -> {value_str}", "debug")
+        try:
+            cookie_string = self.cookie_text.get(1.0, tk.END).strip()
+            if not cookie_string:
+                self.log("  ❌ Cookie为空,无法上传图片", "error")
+                return None
+            
+            cookies = self.parse_cookie(cookie_string)
+            csrf_token = self._get_csrf_token()
+            
+            self.log(f"  🔍 使用的CSRF token: {csrf_token[:30] if csrf_token else 'None'}...", "debug")
+            self.log(f"  🔍 图片大小: {len(image_data)} bytes", "debug")
+            
+            # 构建通用请求头
+            base_headers = {
+                "accept": "application/json, text/plain, */*",
+                "accept-language": "zh-CN,zh;q=0.9",
+                "f-version": "docs-6-17-1781628518399",
+                "x-csrftoken": csrf_token,
+                "x-tt-logid": "02178275091573600000000000000000000ffff5bd640caae0cb8",
+                "x-tt-trace": "1",
+                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
+            }
+            
+            # ========== 步骤1: 获取uploadCode ==========
+            self.log(f"  📤 [1/4] 获取上传code...", "info")
+            code_url = "https://xhd.feishu.cn/space/api/bitable/external/share/uploadCode"
+            code_params = {
+                "shareToken": self.share_token,
+                "fileName": filename,
+                "size": len(image_data),
+                "mountPoint": "bitable_tmp_point"
+            }
+            
+            self.log(f"  📋 请求URL: {code_url}", "debug")
+            self.log(f"  📋 请求参数: {json.dumps(code_params, ensure_ascii=False)}", "debug")
+            
+            code_response = requests.get(code_url, params=code_params, headers=base_headers, cookies=cookies, timeout=30)
+            
+            self.log(f"  📡 响应状态码: {code_response.status_code}", "debug")
+            
+            if code_response.status_code != 200:
+                self.log(f"  ❌ 获取code失败: HTTP {code_response.status_code}", "error")
+                self.log(f"  ❌ 响应内容: {code_response.text}", "error")
+                return None
+            
+            try:
+                code_data = code_response.json()
+            except Exception as e:
+                self.log(f"  ❌ 解析JSON失败: {str(e)}", "error")
+                return None
+            
+            upload_code = code_data.get('data', {}).get('uploadCode')
+            if not upload_code:
+                self.log(f"  ❌ 响应中没有uploadCode字段", "error")
+                self.log(f"  ❌ data内容: {code_data.get('data', {})}", "error")
+                return None
+            
+            self.log(f"  ✅ 获取uploadCode成功: {upload_code[:30]}...", "success")
+            
+            # ========== 步骤2: 准备上传 ==========
+            self.log(f"  📤 [2/4] 准备上传,获取upload_id...", "info")
+            prepare_url = "https://xhd.feishu.cn/space/api/box/upload/prepare/authcode/"
+            prepare_headers = base_headers.copy()
+            prepare_headers["content-type"] = "application/json"
+            prepare_headers["x-command"] = "space.api.box.upload.prepare.authcode"
+            
+            prepare_body = {
+                "code": upload_code,
+                "mount_point": "bitable_tmp_point",
+                "mount_node_token": self.share_token
+            }
+            
+            self.log(f"  📋 请求体: {json.dumps(prepare_body, ensure_ascii=False)}", "debug")
+            
+            prepare_response = requests.post(prepare_url, headers=prepare_headers, cookies=cookies, json=prepare_body, timeout=30)
+            
+            self.log(f"  📡 响应状态码: {prepare_response.status_code}", "debug")
+            
+            if prepare_response.status_code != 200:
+                self.log(f"  ❌ 准备上传失败: HTTP {prepare_response.status_code}", "error")
+                self.log(f"  ❌ 响应内容: {prepare_response.text}", "error")
+                return None
+            
+            try:
+                prepare_data = prepare_response.json()
+            except Exception as e:
+                self.log(f"  ❌ 解析JSON失败: {str(e)}", "error")
+                return None
+            
+            if prepare_data.get('code') != 0:
+                self.log(f"  ❌ 准备上传失败: code={prepare_data.get('code')}, msg={prepare_data.get('msg', '未知错误')}", "error")
+                return None
+            
+            upload_id = prepare_data.get('data', {}).get('upload_id')
+            if not upload_id:
+                self.log(f"  ❌ 响应中没有upload_id字段", "error")
+                self.log(f"  ❌ data内容: {prepare_data.get('data', {})}", "error")
+                return None
+            
+            self.log(f"  ✅ 获取upload_id成功: {upload_id}", "success")
+            
+            self.log(f"  📤 [3/4] 上传文件内容 ({len(image_data)} bytes)...", "info")
+
+            # 使用 Adler-32 计算 checksum(飞书使用的算法)
+            import zlib
+            checksum = zlib.adler32(image_data) & 0xffffffff
+
+            upload_url = f"https://internal-api-drive-stream.feishu.cn/space/api/box/stream/upload/merge_block/?upload_id={upload_id}"
+            upload_headers = base_headers.copy()
+            upload_headers["content-type"] = "application/octet-stream"
+            upload_headers["x-command"] = "space.api.box.stream.upload.merge_block"
+            upload_headers["x-seq-list"] = "0"
+            upload_headers["x-block-list-checksum"] = str(checksum)  # 注意:是数字字符串,不是十六进制
+            upload_headers["x-block-origin-size"] = str(len(image_data))
+
+            self.log(f"  📋 文件大小: {len(image_data)} bytes, checksum(Adler-32): {checksum}", "debug")
+            
+            upload_response = requests.post(upload_url, headers=upload_headers, cookies=cookies, data=image_data, timeout=60)
+            
+            self.log(f"  📡 响应状态码: {upload_response.status_code}", "debug")
+            self.log(f"  📡 响应内容: {upload_response.text[:500]}", "debug")
+            
+            if upload_response.status_code != 200:
+                self.log(f"  ❌ 上传文件失败: HTTP {upload_response.status_code}", "error")
+                self.log(f"  ❌ 响应内容: {upload_response.text}", "error")
+                return None
+            
+            # 检查上传是否真正成功
+            try:
+                upload_result = upload_response.json()
+                if upload_result.get('code') != 0:
+                    self.log(f"  ❌ 上传文件失败: code={upload_result.get('code')}, msg={upload_result.get('message', '未知错误')}", "error")
+                    return None
+            except:
+                pass
+            
+            self.log(f"  ✅ 文件上传成功", "success")
+            
+            # ========== 步骤4: 完成上传 ==========
+            self.log(f"  📤 [4/4] 完成上传,获取file_token...", "info")
+            finish_url = "https://xhd.feishu.cn/space/api/box/upload/finish/"
+            finish_headers = base_headers.copy()
+            finish_headers["content-type"] = "application/json"
+            finish_headers["x-command"] = "space.api.box.upload.finish"
+            
+            finish_body = {
+                "upload_id": upload_id,
+                "num_blocks": 1,
+                "push_open_history_record": 0
+            }
+            
+            self.log(f"  📋 请求体: {json.dumps(finish_body, ensure_ascii=False)}", "debug")
+            
+            finish_response = requests.post(finish_url, headers=finish_headers, cookies=cookies, json=finish_body, timeout=30)
+            
+            self.log(f"  📡 响应状态码: {finish_response.status_code}", "debug")
+            self.log(f"  📡 响应内容: {finish_response.text[:500]}", "debug")
+            
+            if finish_response.status_code != 200:
+                self.log(f"  ❌ 完成上传失败: HTTP {finish_response.status_code}", "error")
+                self.log(f"  ❌ 响应内容: {finish_response.text}", "error")
+                return None
+            
+            try:
+                finish_data = finish_response.json()
+            except Exception as e:
+                self.log(f"  ❌ 解析JSON失败: {str(e)}", "error")
+                return None
+            
+            if finish_data.get('code') != 0:
+                self.log(f"  ❌ 完成上传失败: code={finish_data.get('code')}, msg={finish_data.get('message', '未知错误')}", "error")
+                self.log(f"  ❌ 完整响应: {json.dumps(finish_data, ensure_ascii=False, indent=2)}", "error")
+                return None
+            
+            file_token = finish_data.get('data', {}).get('file_token')
+            if not file_token:
+                self.log(f"  ❌ 响应中没有file_token字段", "error")
+                self.log(f"  ❌ data内容: {finish_data.get('data', {})}", "error")
+                return None
+            
+            # 获取图片信息
+            width, height = 0, 0
+            try:
+                from PIL import Image
+                import io
+                pil_img = Image.open(io.BytesIO(image_data))
+                width, height = pil_img.size
+            except:
+                pass
+            
+            result = {
+                'attachmentToken': file_token,
+                'id': file_token,
+                'mimeType': self._get_mime_type(filename),
+                'name': filename,
+                'size': len(image_data),
+                'timeStamp': int(time.time() * 1000),
+                'height': height,
+                'width': width
+            }
+            
+            self.log(f"  ✅ 图片上传完成! file_token: {file_token}", "success")
+            return result
+            
+        except requests.exceptions.Timeout:
+            self.log(f"  ❌ 上传超时", "error")
+            return None
+        except Exception as e:
+            self.log(f"  ❌ 上传图片异常: {str(e)}", "error")
+            import traceback
+            self.log(f"  详细: {traceback.format_exc()}", "debug")
+            return None
+    
+    def extract_images_to_cache(self, file_path, sheet_name):
+        """
+        提取Excel中的所有图片到缓存
+        返回: dict {行号: [图片数据列表]}
+        """
+        image_cache = {}
+        try:
+            wb = load_workbook(file_path, data_only=True)
+            ws = wb[sheet_name]
+            
+            if hasattr(ws, '_images') and ws._images:
+                self.log(f"  📸 开始提取 {len(ws._images)} 张图片到缓存...", "info")
+                for idx, img in enumerate(ws._images):
+                    try:
+                        # 获取位置信息
+                        if hasattr(img, 'anchor') and hasattr(img.anchor, '_from'):
+                            anchor_from = img.anchor._from
+                            row = anchor_from.row + 1
+                            col = anchor_from.col + 1
                         else:
-                            self.log(f"  ℹ️ 列 '{col}' 包含非图片数据: {value_str[:50]}...", "debug")
-        
-        if not image_fields:
-            self.log("  📝 未检测到图片字段", "debug")
+                            row = idx + 1
+                            col = 0
+                        
+                        # 获取图片数据
+                        image_data = None
+                        img_format = 'jpg'
+                        filename = f"image_{row}_{idx}.jpg"
+                        
+                        if hasattr(img, '_data') and callable(img._data):
+                            image_data = img._data()
+                        elif hasattr(img, '_data') and not callable(img._data):
+                            image_data = img._data
+                        elif hasattr(img, 'image'):
+                            if hasattr(img.image, '_data') and callable(img.image._data):
+                                image_data = img.image._data()
+                            elif hasattr(img.image, '_data'):
+                                image_data = img.image._data
+                        
+                        if image_data is None:
+                            continue
+                        
+                        # 检测图片格式
+                        try:
+                            from PIL import Image
+                            import io
+                            pil_img = Image.open(io.BytesIO(image_data))
+                            img_format = pil_img.format if pil_img.format else 'jpg'
+                            filename = f"image_{row}_{idx}.{img_format.lower()}"
+                        except:
+                            pass
+                        
+                        if row not in image_cache:
+                            image_cache[row] = []
+                        image_cache[row].append({
+                            'data': image_data,
+                            'col': col,
+                            'format': img_format,
+                            'filename': filename
+                        })
+                        
+                    except Exception as e:
+                        self.log(f"  ⚠️ 提取图片失败: {str(e)}", "warning")
+            
+            wb.close()
+            
+            total = sum(len(imgs) for imgs in image_cache.values())
+            self.log(f"  ✅ 成功提取 {total} 张图片到缓存,分布在 {len(image_cache)} 行", "success")
+            
+        except Exception as e:
+            self.log(f"  ❌ 提取图片失败: {str(e)}", "error")
         
-        return image_fields
+        return image_cache
     
-    def build_request_body(self, row):
+    def build_request_body(self, row_dict, row_index):
         """根据Excel行数据构建请求体"""
         try:
             # 获取各列数据
-            name = str(row.get("姓名", "")) if pd.notna(row.get("姓名")) else ""
-            gender = str(row.get("性别", "")) if pd.notna(row.get("性别")) else ""
-            phone = str(row.get("手机号码", "")) if pd.notna(row.get("手机号码")) else ""
-            remark = str(row.get("备注", "")) if pd.notna(row.get("备注")) else ""
-            institution = str(row.get("机构", "")) if pd.notna(row.get("机构")) else ""
-            target_school = str(row.get("意向学校", "")) if pd.notna(row.get("意向学校")) else ""
-            consulting = str(row.get("咨询项目", "")) if pd.notna(row.get("咨询项目")) else ""
-            product = str(row.get("产品", "")) if pd.notna(row.get("产品")) else ""
-            source = str(row.get("线索来源", "")) if pd.notna(row.get("线索来源")) else ""
-            market_person = str(row.get("市场人员", "")) if pd.notna(row.get("市场人员")) else ""
-            
-            # 提取图片信息
-            self.log("  📷 检查图片字段...", "debug")
-            image_info = self.extract_image_info(row)
-            
-            if image_info:
-                self.log(f"  ✅ 找到 {len(image_info)} 个图片字段: {', '.join(image_info.keys())}", "debug")
-                # 这里可以添加图片上传逻辑
-                # 注意:飞书可能使用特定的字段ID存储图片
-                # 你需要根据实际的字段ID来设置
-            else:
-                self.log("  ⚠️ 未找到图片字段,请检查Excel列名是否包含'图片'、'照片'等关键词", "warning")
+            name = str(row_dict.get("姓名", "")) if pd.notna(row_dict.get("姓名")) else ""
+            gender = str(row_dict.get("性别", "")) if pd.notna(row_dict.get("性别")) else ""
+            phone = str(row_dict.get("手机号码", "")) if pd.notna(row_dict.get("手机号码")) else ""
+            remark = str(row_dict.get("备注", "")) if pd.notna(row_dict.get("备注")) else ""
+            institution = str(row_dict.get("机构", "")) if pd.notna(row_dict.get("机构")) else ""
+            target_school = str(row_dict.get("意向学校", "")) if pd.notna(row_dict.get("意向学校")) else ""
+            consulting = str(row_dict.get("咨询项目", "")) if pd.notna(row_dict.get("咨询项目")) else ""
+            product = str(row_dict.get("产品", "")) if pd.notna(row_dict.get("产品")) else ""
+            source = str(row_dict.get("线索来源", "")) if pd.notna(row_dict.get("线索来源")) else ""
+            market_person = str(row_dict.get("市场人员", "")) if pd.notna(row_dict.get("市场人员")) else ""
+            
+            self.log(f"  📝 提取数据: 姓名={name}, 手机={phone}", "debug")
             
-            # 构建备注文本(包含所有信息)
+            # 构建备注文本
             remark_text = f"姓名:{name},性别:{gender},手机号码:{phone}"
             if remark:
                 remark_text += f",{remark}"
@@ -321,7 +592,7 @@ class FeishuSubmitApp:
                     "value": [{"type": "text", "text": remark_text}]
                 },
                 "fldexDPwcE": {"type": 3, "value": "optmlZt00z"} if institution else {"type": 3},
-                "fldWt6JrgK": {"type": 3, "value": "optTD4UQsM"} if target_school else {"type": 3},
+                "fldWt6JrgK": {"type": 3, "value": "optqw2jaJ5"} if target_school else {"type": 3},
                 "fldirDDoLV": {"type": 4, "value": ["opt96MRele"]} if consulting else {"type": 4},
                 "fldudfcNVO": {"type": 4, "value": ["optgwn1azI"]} if product else {"type": 4},
                 "fldS90QCXo": {"type": 1, "value": [{"type": "text", "text": name}]} if name else {"type": 1},
@@ -341,7 +612,7 @@ class FeishuSubmitApp:
                         "users": [{
                             "userId": "7646228072081099729",
                             "name": "俞玥戎",
-                            "avatarUrl": "https://s1-imfile.feishucdn.com/static-resource/v1/v3_00128_e0f4db42-191a-4878-8546-67a4dcb9005g~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
+                            "avatarUrl": "https://s3-imfile.feishucdn.com/static-resource/v1/v3_00128_e0f4db42-191a-4878-8546-67a4dcb9005g~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
                             "enName": "俞玥戎",
                             "notify": False
                         }]
@@ -349,23 +620,44 @@ class FeishuSubmitApp:
                 } if market_person else {"type": 11},
                 "flde7thxT7": {"type": 3, "value": "optKdksJ5a"},
                 "fldaxavu52": {"type": 3, "value": "optHsn4zKU"},
-                "fldMpwPaI1": {"type": 17}
+                "fldMpwPaI1": {"type": 17}  # 图片字段,默认空
             }
             
-            # 如果检测到图片,尝试添加到data_obj中
-            # 注意:你需要知道飞书表格中图片字段的实际ID
-            # 这里假设图片字段ID为 fldImageXxx,你需要根据实际情况修改
-            if image_info:
-                # 这里需要根据实际的字段ID来设置
-                # 示例:如果有图片,添加到某个字段
-                # for field_name, info in image_info.items():
-                #     if info['type'] == 'file':
-                #         # 处理文件上传
-                #         pass
-                #     elif info['type'] == 'url':
-                #         # 处理URL
-                #         pass
-                self.log("  💡 提示: 请根据实际字段ID配置图片上传", "info")
+            # ========== 检查并处理图片 ==========
+            if self.image_cache and row_index in self.image_cache:
+                images = self.image_cache[row_index]
+                self.log(f"  🖼️ 检测到 {len(images)} 个嵌入图片在第 {row_index} 行", "info")
+                
+                # 上传第一张图片
+                if images:
+                    img_info = images[0]
+                    
+                    # 检查是否已经上传过
+                    if row_index in self.uploaded_cache:
+                        img_value = self.uploaded_cache[row_index]
+                        self.log(f"  ♻️ 使用已上传的图片: {img_value.get('attachmentToken')}", "debug")
+                    else:
+                        # 上传图片
+                        self.log(f"  📤 开始上传图片...", "info")
+                        uploaded = self.upload_image(img_info['data'], img_info['filename'])
+                        if uploaded:
+                            self.uploaded_cache[row_index] = uploaded
+                            img_value = uploaded
+                            self.log(f"  ✅ 图片上传成功,token: {uploaded.get('attachmentToken')}", "success")
+                        else:
+                            self.log(f"  ⚠️ 图片上传失败,跳过该图片", "warning")
+                            img_value = None
+                    
+                    # 如果上传成功,添加到data_obj
+                    if img_value:
+                        data_obj["fldMpwPaI1"] = {
+                            "type": 17,
+                            "value": [img_value]
+                        }
+                        self.log(f"  📎 图片已添加到请求中", "debug")
+            else:
+                # 没有图片,确保图片字段为空
+                data_obj["fldMpwPaI1"] = {"type": 17}
             
             # 构建完整的请求体
             body = {
@@ -377,12 +669,13 @@ class FeishuSubmitApp:
             return body
         except Exception as e:
             self.log(f"构建请求体失败: {str(e)}", "error")
+            import traceback
+            self.log(f"详细错误: {traceback.format_exc()}", "debug")
             return None
     
     def send_request(self, row_data, row_index):
         """发送单条请求"""
         try:
-            # 检查是否有图片字段
             self.log(f"\n{'='*50}", "info")
             self.log(f"📤 准备发送第 {row_index} 行数据", "info")
             
@@ -392,7 +685,7 @@ class FeishuSubmitApp:
                 return False, "Cookie为空"
             
             # 构建请求体
-            body = self.build_request_body(row_data)
+            body = self.build_request_body(row_data, row_index)
             if body is None:
                 self.log("❌ 构建请求体失败", "error")
                 return False, "构建请求体失败"
@@ -401,20 +694,14 @@ class FeishuSubmitApp:
             body_str = json.dumps(body, ensure_ascii=False)
             self.log(f"📦 请求体大小: {len(body_str)} 字节", "debug")
             
-            # 检查data字段中的图片内容
-            data_str = body.get('data', '')
-            if data_str:
-                # 检查是否包含图片相关的字段
-                if 'fld' in data_str and ('image' in data_str.lower() or 'picture' in data_str.lower()):
-                    self.log("  🖼️ 请求中包含图片相关字段", "debug")
-                else:
-                    self.log("  📝 请求中未检测到图片字段", "debug")
-            
             # 准备请求
             headers = self.default_headers.copy()
-            # 更新Cookie
             cookies = self.parse_cookie(cookie_string)
             
+            # 更新动态头
+            headers["x-csrftoken"] = self._get_csrf_token()
+            headers["f-version"] = "docs-6-17-1781628518399"
+            
             self.log(f"🌐 发送请求到: {self.url}", "debug")
             
             # 发送请求
@@ -426,16 +713,13 @@ class FeishuSubmitApp:
                 timeout=30
             )
             
-            # 记录响应状态
             self.log(f"📡 响应状态码: {response.status_code}", "debug")
-            self.log(f"📋 响应头: {dict(response.headers)}", "debug")
             
             # 解析响应
             try:
                 resp_data = response.json()
                 resp_text = json.dumps(resp_data, ensure_ascii=False, indent=2)
                 
-                # 检查响应中是否有错误信息
                 if isinstance(resp_data, dict):
                     if resp_data.get('code') != 0 and resp_data.get('code') is not None:
                         self.log(f"⚠️ 服务器返回错误码: {resp_data.get('code')}", "warning")
@@ -448,17 +732,6 @@ class FeishuSubmitApp:
             
             if response.status_code == 200:
                 self.log(f"✅ 第{row_index}行 发送成功", "success")
-                # 检查返回的data中是否包含图片上传信息
-                try:
-                    resp_json = response.json()
-                    if 'data' in resp_json:
-                        data_content = resp_json['data']
-                        if isinstance(data_content, dict):
-                            # 检查是否有图片上传相关信息
-                            if 'image' in str(data_content).lower() or 'picture' in str(data_content).lower():
-                                self.log("  🖼️ 响应中包含图片处理信息", "debug")
-                except:
-                    pass
                 return True, resp_text
             else:
                 self.log(f"❌ 第{row_index}行 发送失败 (HTTP {response.status_code})", "error")
@@ -473,6 +746,8 @@ class FeishuSubmitApp:
             return False, "连接失败"
         except Exception as e:
             self.log(f"💥 第{row_index}行 异常: {str(e)}", "error")
+            import traceback
+            self.log(f"详细错误: {traceback.format_exc()}", "debug")
             return False, str(e)
         finally:
             self.log(f"{'='*50}\n", "info")
@@ -480,43 +755,40 @@ class FeishuSubmitApp:
     def send_all(self):
         """发送所有数据的主循环"""
         try:
-            # 获取配置
             excel_file = self.file_path_var.get()
             sheet_name = self.sheet_var.get()
             min_interval = float(self.min_interval_var.get())
             max_interval = float(self.max_interval_var.get())
-            start_row = int(self.start_row_var.get()) - 1  # 转为0索引
+            start_row = int(self.start_row_var.get()) - 1
             
             if not excel_file:
                 self.log("❌ 请先选择Excel文件", "error")
                 self.status_var.set("❌ 请先选择Excel文件")
                 return
             
-            # 读取Excel
+            # 清空上次上传缓存
+            self.uploaded_cache = {}
+            
+            # ========== 自动提取所有图片到缓存 ==========
+            self.log(f"🔍 正在自动提取Excel中的嵌入图片...", "info")
+            self.image_cache = self.extract_images_to_cache(excel_file, sheet_name)
+            
+            if self.image_cache:
+                total_images = sum(len(imgs) for imgs in self.image_cache.values())
+                self.log(f"📊 共提取 {total_images} 张图片,分布在 {len(self.image_cache)} 行", "info")
+                rows_with_images = sorted(self.image_cache.keys())
+                if len(rows_with_images) <= 20:
+                    self.log(f"📋 有图片的行: {rows_with_images}", "debug")
+                else:
+                    self.log(f"📋 有图片的行: {rows_with_images[:20]} ... (共{len(rows_with_images)}行)", "debug")
+            else:
+                self.log(f"📷 未检测到任何嵌入图片", "info")
+            
+            # ========== 读取Excel数据 ==========
             try:
                 df = pd.read_excel(excel_file, sheet_name=sheet_name)
                 self.log(f"📊 成功读取Excel: {len(df)} 行数据", "info")
                 self.log(f"📋 列名: {', '.join(df.columns.tolist())}", "info")
-                
-                # 检查是否有图片相关列
-                image_cols = [col for col in df.columns if any(kw in str(col).lower() for kw in ['图', '片', '照', '像', 'image', 'photo', 'pic', 'img', '附件'])]
-                if image_cols:
-                    self.log(f"📷 发现图片相关列: {', '.join(image_cols)}", "info")
-                    
-                    # 检查这些列是否有数据
-                    for col in image_cols:
-                        non_empty = df[col].notna().sum()
-                        if non_empty > 0:
-                            self.log(f"  ✅ 列 '{col}' 有 {non_empty} 行数据", "info")
-                            # 显示一些示例数据
-                            sample = df[col].dropna().iloc[0] if non_empty > 0 else None
-                            if sample:
-                                self.log(f"  📎 示例数据: {str(sample)[:100]}...", "debug")
-                        else:
-                            self.log(f"  ⚠️ 列 '{col}' 为空", "warning")
-                else:
-                    self.log("⚠️ 未发现图片相关列,请检查列名是否包含'图片'、'照片'等关键词", "warning")
-                    
             except Exception as e:
                 self.log(f"❌ 读取Excel失败: {str(e)}", "error")
                 self.status_var.set(f"❌ 读取失败: {str(e)}")
@@ -527,7 +799,6 @@ class FeishuSubmitApp:
                 self.status_var.set("⚠️ 没有数据")
                 return
             
-            # 从指定行开始
             df = df.iloc[start_row:]
             total = len(df)
             success_count = 0
@@ -542,30 +813,38 @@ class FeishuSubmitApp:
                     self.status_var.set("⏹️ 已停止")
                     break
                 
-                row_index = start_row + idx + 1  # 实际行号
+                # ===== 关键修复:计算Excel实际行号 =====
+                # DataFrame的索引 + 2(因为第1行是表头,DataFrame从0开始)
+                # 再加上start_row(起始行的偏移)
+                excel_row_number = idx + 2 + start_row
+                
+                self.log(f"  📍 DataFrame索引: {idx}, Excel行号: {excel_row_number}", "debug")
+                
+                # 检查该行是否有图片(使用Excel实际行号)
+                if self.image_cache and excel_row_number in self.image_cache:
+                    img_count = len(self.image_cache[excel_row_number])
+                    self.log(f"  🖼️ 第{excel_row_number}行有 {img_count} 个嵌入图片,将自动上传并添加", "info")
                 
-                # 发送请求
-                success, resp_text = self.send_request(row.to_dict(), row_index)
+                success, resp_text = self.send_request(row.to_dict(), excel_row_number)
                 if success:
                     success_count += 1
                 else:
                     fail_count += 1
                 
-                # 更新状态
                 self.status_var.set(f"🔄 发送中... {idx+1}/{total} (成功:{success_count} 失败:{fail_count})")
                 
-                # 随机间隔(最后一条不等待)
                 if idx < total - 1 and not self.stop_flag:
                     interval = random.uniform(min_interval, max_interval)
                     self.log(f"⏳ 等待 {interval:.1f} 秒...", "info")
                     time.sleep(interval)
             
-            # 完成统计
             self.log(f"🏁 发送完成!成功: {success_count}, 失败: {fail_count}", "success" if fail_count == 0 else "warning")
             self.status_var.set(f"✅ 完成 成功:{success_count} 失败:{fail_count}")
             
         except Exception as e:
             self.log(f"💥 程序异常: {str(e)}", "error")
+            import traceback
+            self.log(f"详细错误: {traceback.format_exc()}", "debug")
             self.status_var.set(f"❌ 异常: {str(e)}")
         finally:
             self.is_running = False
@@ -577,7 +856,6 @@ class FeishuSubmitApp:
         if self.is_running:
             return
         
-        # 验证配置
         if not self.file_path_var.get():
             messagebox.showwarning("提示", "请先选择Excel文件")
             return
@@ -595,14 +873,12 @@ class FeishuSubmitApp:
             messagebox.showerror("错误", "间隔或行号请输入有效数字")
             return
         
-        # 重置状态
         self.is_running = True
         self.stop_flag = False
         self.start_btn.config(state=tk.DISABLED)
         self.stop_btn.config(state=tk.NORMAL)
         self.status_var.set("🔄 正在发送...")
         
-        # 启动线程
         thread = threading.Thread(target=self.send_all, daemon=True)
         thread.start()
     
@@ -612,6 +888,147 @@ class FeishuSubmitApp:
         self.log("⏹️ 正在停止...", "warning")
         self.stop_btn.config(state=tk.DISABLED)
 
+    # ========== 提取图片功能(手动) ==========
+    def extract_images_to_folder(self, file_path, sheet_name, output_folder=None):
+        """提取Excel中的所有图片并保存到文件夹"""
+        try:
+            if output_folder is None:
+                base_dir = os.path.dirname(file_path)
+                base_name = os.path.splitext(os.path.basename(file_path))[0]
+                output_folder = os.path.join(base_dir, f"{base_name}_提取的图片")
+            
+            if not os.path.exists(output_folder):
+                os.makedirs(output_folder)
+            
+            mapping_file = os.path.join(output_folder, "图片映射表.txt")
+            wb = load_workbook(file_path, data_only=True)
+            ws = wb[sheet_name]
+            
+            image_count = 0
+            error_count = 0
+            
+            with open(mapping_file, 'w', encoding='utf-8') as f:
+                f.write("="*60 + "\n")
+                f.write("Excel图片提取映射表\n")
+                f.write(f"源文件: {file_path}\n")
+                f.write(f"工作表: {sheet_name}\n")
+                f.write(f"提取时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
+                f.write("="*60 + "\n\n")
+                f.write("行号\t列号\t列字母\t文件名\t尺寸\t格式\n")
+                f.write("-"*60 + "\n")
+                
+                if hasattr(ws, '_images') and ws._images:
+                    self.log(f"  📸 开始提取 {len(ws._images)} 张图片...", "debug")
+                    for idx, img in enumerate(ws._images):
+                        try:
+                            if hasattr(img, 'anchor') and hasattr(img.anchor, '_from'):
+                                anchor_from = img.anchor._from
+                                row = anchor_from.row + 1
+                                col = anchor_from.col + 1
+                                col_letter = get_column_letter(col)
+                            else:
+                                row = idx + 1
+                                col = 0
+                                col_letter = '?'
+                            
+                            image_data = None
+                            if hasattr(img, '_data') and callable(img._data):
+                                image_data = img._data()
+                            elif hasattr(img, '_data') and not callable(img._data):
+                                image_data = img._data
+                            elif hasattr(img, 'image'):
+                                if hasattr(img.image, '_data') and callable(img.image._data):
+                                    image_data = img.image._data()
+                                elif hasattr(img.image, '_data'):
+                                    image_data = img.image._data
+                            
+                            if image_data is None:
+                                error_count += 1
+                                continue
+                            
+                            # 检测格式并保存
+                            img_format = 'jpg'
+                            width, height = 0, 0
+                            try:
+                                from PIL import Image
+                                import io
+                                pil_img = Image.open(io.BytesIO(image_data))
+                                img_format = pil_img.format if pil_img.format else 'jpg'
+                                width, height = pil_img.size
+                                filename = f"第{row}行_列{col_letter}_{idx+1}.{img_format.lower()}"
+                                filepath = os.path.join(output_folder, filename)
+                                pil_img.save(filepath)
+                            except:
+                                filename = f"第{row}行_列{col_letter}_{idx+1}.jpg"
+                                filepath = os.path.join(output_folder, filename)
+                                with open(filepath, 'wb') as f_out:
+                                    f_out.write(image_data)
+                            
+                            f.write(f"{row}\t{col}\t{col_letter}\t{filename}\t{width}x{height}\t{img_format}\n")
+                            image_count += 1
+                            
+                            if image_count % 50 == 0:
+                                self.log(f"  📸 已提取 {image_count} 张图片...", "debug")
+                        except Exception as e:
+                            error_count += 1
+                            self.log(f"  ⚠️ 提取图片失败: {str(e)}", "warning")
+            
+            wb.close()
+            
+            with open(mapping_file, 'a', encoding='utf-8') as f:
+                f.write("\n" + "="*60 + "\n")
+                f.write(f"提取完成: 成功 {image_count} 张, 失败 {error_count} 张\n")
+                f.write("="*60 + "\n")
+            
+            self.log(f"✅ 成功提取 {image_count} 张图片到: {output_folder}", "success")
+            if error_count > 0:
+                self.log(f"⚠️ 有 {error_count} 张图片提取失败", "warning")
+            self.log(f"📋 映射文件: {mapping_file}", "info")
+            
+            return output_folder
+            
+        except Exception as e:
+            self.log(f"❌ 提取图片失败: {str(e)}", "error")
+            return None
+    
+    def extract_images(self):
+        """手动提取图片的入口方法"""
+        excel_file = self.file_path_var.get()
+        if not excel_file:
+            messagebox.showwarning("提示", "请先选择Excel文件")
+            return
+        
+        sheet_name = self.sheet_var.get()
+        
+        if not messagebox.askyesno("确认", 
+            f"即将提取Excel中的所有图片\n\n"
+            f"文件: {os.path.basename(excel_file)}\n"
+            f"工作表: {sheet_name}\n\n"
+            f"是否继续?"):
+            return
+        
+        def extract_thread():
+            self.log("📸 开始提取图片...", "info")
+            self.status_var.set("🔄 正在提取图片...")
+            self.start_btn.config(state=tk.DISABLED)
+            
+            output_folder = self.extract_images_to_folder(excel_file, sheet_name)
+            
+            if output_folder:
+                self.status_var.set(f"✅ 图片已提取到: {output_folder}")
+                messagebox.showinfo("提取成功", 
+                    f"图片已成功提取!\n\n"
+                    f"保存位置: {output_folder}\n\n"
+                    f"请查看文件夹中的 '图片映射表.txt' 文件")
+            else:
+                self.status_var.set("❌ 提取失败")
+                messagebox.showerror("提取失败", "图片提取失败,请查看日志了解详情。")
+            
+            self.start_btn.config(state=tk.NORMAL)
+        
+        thread = threading.Thread(target=extract_thread, daemon=True)
+        thread.start()
+
 
 if __name__ == "__main__":
     root = tk.Tk()

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä