PUGE 1 mese fa
parent
commit
41cbea4b27
9 ha cambiato i file con 2970 aggiunte e 366 eliminazioni
  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"
+}

File diff suppressed because it is too large
+ 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 requests
 import json
 import json
 import time
 import time
+import threading
+import os
+import re
+import ssl
+import subprocess
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
 from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                              QHBoxLayout, QLabel, QLineEdit, QPushButton, 
                              QHBoxLayout, QLabel, QLineEdit, QPushButton, 
                              QTableWidget, QTableWidgetItem, QHeaderView, 
                              QTableWidget, QTableWidgetItem, QHeaderView, 
                              QCheckBox, QMessageBox, QGroupBox, QTextEdit,
                              QCheckBox, QMessageBox, QGroupBox, QTextEdit,
-                             QGridLayout, QSplitter)
+                             QGridLayout, QSplitter, QComboBox, QFileDialog)
 from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
 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):
 class LoginThread(QThread):
-    """登录线程"""
     finished = pyqtSignal(dict)
     finished = pyqtSignal(dict)
     error = pyqtSignal(str)
     error = pyqtSignal(str)
     
     
@@ -26,10 +220,8 @@ class LoginThread(QThread):
         try:
         try:
             md5_password = hashlib.md5(self.password.encode('utf-8')).hexdigest()
             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"
             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 = requests.get(url, timeout=10)
             response.encoding = 'utf-8'
             response.encoding = 'utf-8'
-            
             if response.status_code == 200:
             if response.status_code == 200:
                 data = response.json()
                 data = response.json()
                 if data.get('rspCode') == '1':
                 if data.get('rspCode') == '1':
@@ -41,8 +233,9 @@ class LoginThread(QThread):
         except Exception as e:
         except Exception as e:
             self.error.emit(f"登录异常: {str(e)}")
             self.error.emit(f"登录异常: {str(e)}")
 
 
+# ==================== 查询车辆线程 ====================
+
 class QueryThread(QThread):
 class QueryThread(QThread):
-    """查询车辆定位线程"""
     finished = pyqtSignal(dict)
     finished = pyqtSignal(dict)
     error = pyqtSignal(str)
     error = pyqtSignal(str)
     
     
@@ -53,10 +246,8 @@ class QueryThread(QThread):
     def run(self):
     def run(self):
         try:
         try:
             url = f"http://119.91.58.175:9999/gps-web/api/get_gps_r.jsp?sessionId={self.session_id}&teamId=&carIds=&simple=false"
             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 = requests.get(url, timeout=15)
             response.encoding = 'utf-8'
             response.encoding = 'utf-8'
-            
             if response.status_code == 200:
             if response.status_code == 200:
                 data = response.json()
                 data = response.json()
                 if data.get('rspCode') == '1':
                 if data.get('rspCode') == '1':
@@ -68,158 +259,541 @@ class QueryThread(QThread):
         except Exception as e:
         except Exception as e:
             self.error.emit(f"查询异常: {str(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):
 class MainWindow(QMainWindow):
     def __init__(self):
     def __init__(self):
         super().__init__()
         super().__init__()
         self.session_id = None
         self.session_id = None
         self.username = None
         self.username = None
         self.car_data = []
         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.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.initUI()
+        self.load_config()
     
     
     def initUI(self):
     def initUI(self):
         self.setWindowTitle('车辆定位监控系统')
         self.setWindowTitle('车辆定位监控系统')
-        self.setGeometry(100, 100, 1400, 800)
+        self.setGeometry(100, 100, 1500, 850)
         
         
-        # 中央窗口
         central_widget = QWidget()
         central_widget = QWidget()
         self.setCentralWidget(central_widget)
         self.setCentralWidget(central_widget)
         main_layout = QVBoxLayout(central_widget)
         main_layout = QVBoxLayout(central_widget)
         
         
-        # 登录区域
+        # ===== 登录区域 =====
         login_group = QGroupBox("登录")
         login_group = QGroupBox("登录")
         login_layout = QHBoxLayout()
         login_layout = QHBoxLayout()
-        
         login_layout.addWidget(QLabel("账号:"))
         login_layout.addWidget(QLabel("账号:"))
         self.username_input = QLineEdit()
         self.username_input = QLineEdit()
-        self.username_input.setPlaceholderText("请输入账号")
         self.username_input.setText("深金川")
         self.username_input.setText("深金川")
         login_layout.addWidget(self.username_input)
         login_layout.addWidget(self.username_input)
-        
         login_layout.addWidget(QLabel("密码:"))
         login_layout.addWidget(QLabel("密码:"))
         self.password_input = QLineEdit()
         self.password_input = QLineEdit()
-        self.password_input.setPlaceholderText("请输入密码")
         self.password_input.setEchoMode(QLineEdit.Password)
         self.password_input.setEchoMode(QLineEdit.Password)
         self.password_input.setText("123456")
         self.password_input.setText("123456")
         login_layout.addWidget(self.password_input)
         login_layout.addWidget(self.password_input)
-        
         self.login_btn = QPushButton("登录")
         self.login_btn = QPushButton("登录")
         self.login_btn.clicked.connect(self.handle_login)
         self.login_btn.clicked.connect(self.handle_login)
         login_layout.addWidget(self.login_btn)
         login_layout.addWidget(self.login_btn)
-        
         self.status_label = QLabel("未登录")
         self.status_label = QLabel("未登录")
         login_layout.addWidget(self.status_label)
         login_layout.addWidget(self.status_label)
-        
         login_layout.addStretch()
         login_layout.addStretch()
         login_group.setLayout(login_layout)
         login_group.setLayout(login_layout)
         main_layout.addWidget(login_group)
         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 = QPushButton("启动监控")
         self.start_btn.clicked.connect(self.start_monitoring)
         self.start_btn.clicked.connect(self.start_monitoring)
         self.start_btn.setEnabled(False)
         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 = QPushButton("停止监控")
         self.stop_btn.clicked.connect(self.stop_monitoring)
         self.stop_btn.clicked.connect(self.stop_monitoring)
         self.stop_btn.setEnabled(False)
         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)
         splitter = QSplitter(Qt.Vertical)
         
         
-        # 车辆信息表格
         table_widget = QWidget()
         table_widget = QWidget()
         table_layout = QVBoxLayout(table_widget)
         table_layout = QVBoxLayout(table_widget)
         table_layout.setContentsMargins(0, 0, 0, 0)
         table_layout.setContentsMargins(0, 0, 0, 0)
         
         
         self.table = QTableWidget()
         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)
         self.table.setAlternatingRowColors(True)
         table_layout.addWidget(self.table)
         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)
         splitter.addWidget(table_widget)
         
         
-        # 日志输出框
+        # ===== 日志 =====
         log_widget = QWidget()
         log_widget = QWidget()
         log_layout = QVBoxLayout(log_widget)
         log_layout = QVBoxLayout(log_widget)
         log_layout.setContentsMargins(0, 0, 0, 0)
         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 = QTextEdit()
         self.log_output.setReadOnly(True)
         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)
         log_layout.addWidget(self.log_output)
         
         
         splitter.addWidget(log_widget)
         splitter.addWidget(log_widget)
-        splitter.setSizes([500, 300])
-        
+        splitter.setSizes([450, 300])
         main_layout.addWidget(splitter)
         main_layout.addWidget(splitter)
         
         
-        # 状态栏
         self.statusBar().showMessage("就绪")
         self.statusBar().showMessage("就绪")
-        
-        # 初始化触发记录
         self.triggered_records = {}
         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):
     def handle_login(self):
         username = self.username_input.text().strip()
         username = self.username_input.text().strip()
         password = self.password_input.text().strip()
         password = self.password_input.text().strip()
-        
         if not username or not password:
         if not username or not password:
             QMessageBox.warning(self, "警告", "请输入账号和密码")
             QMessageBox.warning(self, "警告", "请输入账号和密码")
             return
             return
-        
         self.login_btn.setEnabled(False)
         self.login_btn.setEnabled(False)
         self.status_label.setText("登录中...")
         self.status_label.setText("登录中...")
-        self.statusBar().showMessage("正在登录...")
-        
         self.login_thread = LoginThread(username, password)
         self.login_thread = LoginThread(username, password)
         self.login_thread.finished.connect(self.on_login_success)
         self.login_thread.finished.connect(self.on_login_success)
         self.login_thread.error.connect(self.on_login_error)
         self.login_thread.error.connect(self.on_login_error)
@@ -227,33 +801,88 @@ class MainWindow(QMainWindow):
     
     
     def on_login_success(self, data):
     def on_login_success(self, data):
         self.session_id = data.get('sessionId')
         self.session_id = data.get('sessionId')
-        self.username = data.get('userName', '')
-        
+        self.username = self.username_input.text().strip()
         if self.session_id:
         if self.session_id:
             self.login_btn.setEnabled(True)
             self.login_btn.setEnabled(True)
             self.status_label.setText(f"已登录: {self.username}")
             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.start_btn.setEnabled(True)
-            self.statusBar().showMessage(f"登录成功,Session ID: {self.session_id[:20]}...")
-            
-            # 登录成功后自动查询
+            self.statusBar().showMessage("登录成功")
+            self.load_regions()
             self.handle_query()
             self.handle_query()
+            
+            # 自动连接WebSocket,使用用户名作为ID
+            self.connect_websocket(self.username)
+            self.save_config()
         else:
         else:
             self.on_login_error("登录响应中没有sessionId")
             self.on_login_error("登录响应中没有sessionId")
     
     
     def on_login_error(self, error_msg):
     def on_login_error(self, error_msg):
         self.login_btn.setEnabled(True)
         self.login_btn.setEnabled(True)
         self.status_label.setText("登录失败")
         self.status_label.setText("登录失败")
-        self.statusBar().showMessage("登录失败")
         QMessageBox.critical(self, "错误", error_msg)
         QMessageBox.critical(self, "错误", error_msg)
     
     
-    def handle_query(self):
+    # ===== 加载区域 =====
+    def load_regions(self):
         if not self.session_id:
         if not self.session_id:
             QMessageBox.warning(self, "警告", "请先登录")
             QMessageBox.warning(self, "警告", "请先登录")
             return
             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 = QueryThread(self.session_id)
         self.query_thread.finished.connect(self.on_query_success)
         self.query_thread.finished.connect(self.on_query_success)
@@ -261,174 +890,445 @@ class MainWindow(QMainWindow):
         self.query_thread.start()
         self.query_thread.start()
     
     
     def on_query_success(self, data):
     def on_query_success(self, data):
-        self.manual_query_btn.setEnabled(True)
-        
         car_list = data.get('list', [])
         car_list = data.get('list', [])
         self.car_data = car_list
         self.car_data = car_list
-        
-        # 保存当前勾选状态
         self.save_check_state()
         self.save_check_state()
-        
         self.table.setRowCount(len(car_list))
         self.table.setRowCount(len(car_list))
         
         
         for row, car in enumerate(car_list):
         for row, car in enumerate(car_list):
             car_plate = car.get('carPlate', '')
             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):
     def on_query_error(self, error_msg):
-        self.manual_query_btn.setEnabled(True)
-        self.statusBar().showMessage("查询失败")
         QMessageBox.critical(self, "错误", error_msg)
         QMessageBox.critical(self, "错误", error_msg)
     
     
+    # ===== 表格操作 =====
     def save_check_state(self):
     def save_check_state(self):
-        """保存当前表格中的勾选状态"""
+        """保存启用和扫码状态"""
         for row in range(self.table.rowCount()):
         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:
                 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)
         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()):
         for row in range(self.table.rowCount()):
             checkbox = self.table.cellWidget(row, 0)
             checkbox = self.table.cellWidget(row, 0)
             if checkbox:
             if checkbox:
                 checkbox.setChecked(True)
                 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:
                 if car_plate:
                     self.car_checked_state[car_plate] = True
                     self.car_checked_state[car_plate] = True
     
     
-    def deselect_all(self):
+    def deselect_all_enable(self):
+        """取消全选启用"""
         for row in range(self.table.rowCount()):
         for row in range(self.table.rowCount()):
             checkbox = self.table.cellWidget(row, 0)
             checkbox = self.table.cellWidget(row, 0)
             if checkbox:
             if checkbox:
                 checkbox.setChecked(False)
                 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:
                 if car_plate:
                     self.car_checked_state[car_plate] = False
                     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):
     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
             return
         
         
         if not self.session_id:
         if not self.session_id:
             QMessageBox.warning(self, "警告", "请先登录")
             QMessageBox.warning(self, "警告", "请先登录")
             return
             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.is_monitoring = True
         self.start_btn.setEnabled(False)
         self.start_btn.setEnabled(False)
         self.stop_btn.setEnabled(True)
         self.stop_btn.setEnabled(True)
-        self.manual_query_btn.setEnabled(False)
+        self.refresh_region_btn.setEnabled(False)
         
         
-        # 清空触发记录
         self.triggered_records = {}
         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("=" * 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.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("监控运行中...")
         self.statusBar().showMessage("监控运行中...")
     
     
     def stop_monitoring(self):
     def stop_monitoring(self):
-        """停止监控"""
         self.is_monitoring = False
         self.is_monitoring = False
         self.refresh_timer.stop()
         self.refresh_timer.stop()
+        self.region_timer.stop()
         self.start_btn.setEnabled(True)
         self.start_btn.setEnabled(True)
         self.stop_btn.setEnabled(False)
         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("监控已停止")
-        self.add_log("=" * 50)
         self.statusBar().showMessage("监控已停止")
         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()
         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
                 continue
             
             
-            # 初始化该车辆的触发记录
+            # 初始化记录
             if car_plate not in self.triggered_records:
             if car_plate not in self.triggered_records:
                 self.triggered_records[car_plate] = {'load': None, 'unload': None}
                 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():
 def main():
     app = QApplication(sys.argv)
     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
 import re
 from datetime import datetime
 from datetime import datetime
 import os
 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:
 class FeishuSubmitApp:
     def __init__(self, root):
     def __init__(self, root):
         self.root = root
         self.root = root
-        self.root.title("飞书表格数据提交工具")
+        self.root.title("飞书表格数据提交工具 - 自动提取图片")
         self.root.geometry("1100x800")
         self.root.geometry("1100x800")
         
         
         # 状态变量
         # 状态变量
@@ -22,16 +29,20 @@ class FeishuSubmitApp:
         self.excel_file = ""
         self.excel_file = ""
         self.cookie_string = ""
         self.cookie_string = ""
         
         
+        # 图片缓存
+        self.image_cache = {}  # {行号: [图片数据列表]}
+        self.image_extract_folder = None
+        self.uploaded_cache = {}  # 缓存已上传的图片token {行号: attachment_info}
+        
         # 从curl提取的默认配置
         # 从curl提取的默认配置
         self.default_headers = {
         self.default_headers = {
             "accept": "application/json, text/plain, */*",
             "accept": "application/json, text/plain, */*",
             "accept-language": "zh-CN,zh;q=0.9",
             "accept-language": "zh-CN,zh;q=0.9",
             "content-type": "application/json",
             "content-type": "application/json",
-            "f-version": "docs-6-9-1780996889854",
+            "f-version": "docs-6-17-1781628518399",
             "origin": "https://xhd.feishu.cn",
             "origin": "https://xhd.feishu.cn",
             "priority": "u=1, i",
             "priority": "u=1, i",
             "referer": "https://xhd.feishu.cn/share/base/form/shrcnJqFUFF0sqlmrfsWpDNf8df",
             "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": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
             "sec-ch-ua-mobile": "?0",
             "sec-ch-ua-mobile": "?0",
             "sec-ch-ua-platform": '"Windows"',
             "sec-ch-ua-platform": '"Windows"',
@@ -40,10 +51,8 @@ class FeishuSubmitApp:
             "sec-fetch-site": "same-origin",
             "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",
             "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-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"
         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)
         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.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)
         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.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)
         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 = ttk.Button(btn_frame, text="⏹ 停止", command=self.stop_send, state=tk.DISABLED)
         self.stop_btn.pack(side=tk.LEFT, padx=(0, 10))
         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="就绪")
         self.status_var = tk.StringVar(value="就绪")
@@ -164,9 +174,7 @@ class FeishuSubmitApp:
         """从剪贴板粘贴Cookie"""
         """从剪贴板粘贴Cookie"""
         try:
         try:
             clipboard_text = self.root.clipboard_get()
             clipboard_text = self.root.clipboard_get()
-            # 尝试从curl命令中提取cookie
             if "cookie:" in clipboard_text.lower() or "-H" in clipboard_text:
             if "cookie:" in clipboard_text.lower() or "-H" in clipboard_text:
-                # 尝试从curl -b 或 -H "cookie: ..." 提取
                 cookie_match = re.search(r'-b\s+"([^"]+)"', clipboard_text)
                 cookie_match = re.search(r'-b\s+"([^"]+)"', clipboard_text)
                 if not cookie_match:
                 if not cookie_match:
                     cookie_match = re.search(r'-H\s+"cookie:\s*([^"]+)"', clipboard_text, re.IGNORECASE)
                     cookie_match = re.search(r'-H\s+"cookie:\s*([^"]+)"', clipboard_text, re.IGNORECASE)
@@ -176,7 +184,6 @@ class FeishuSubmitApp:
                     self.log("已从剪贴板提取Cookie", "success")
                     self.log("已从剪贴板提取Cookie", "success")
                     return
                     return
             
             
-            # 直接粘贴
             self.cookie_text.delete(1.0, tk.END)
             self.cookie_text.delete(1.0, tk.END)
             self.cookie_text.insert(1.0, clipboard_text)
             self.cookie_text.insert(1.0, clipboard_text)
             self.log("已粘贴Cookie", "info")
             self.log("已粘贴Cookie", "info")
@@ -239,77 +246,341 @@ class FeishuSubmitApp:
                 cookies[key.strip()] = value.strip()
                 cookies[key.strip()] = value.strip()
         return cookies
         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:
                         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行数据构建请求体"""
         """根据Excel行数据构建请求体"""
         try:
         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}"
             remark_text = f"姓名:{name},性别:{gender},手机号码:{phone}"
             if remark:
             if remark:
                 remark_text += f",{remark}"
                 remark_text += f",{remark}"
@@ -321,7 +592,7 @@ class FeishuSubmitApp:
                     "value": [{"type": "text", "text": remark_text}]
                     "value": [{"type": "text", "text": remark_text}]
                 },
                 },
                 "fldexDPwcE": {"type": 3, "value": "optmlZt00z"} if institution else {"type": 3},
                 "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},
                 "fldirDDoLV": {"type": 4, "value": ["opt96MRele"]} if consulting else {"type": 4},
                 "fldudfcNVO": {"type": 4, "value": ["optgwn1azI"]} if product 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},
                 "fldS90QCXo": {"type": 1, "value": [{"type": "text", "text": name}]} if name else {"type": 1},
@@ -341,7 +612,7 @@ class FeishuSubmitApp:
                         "users": [{
                         "users": [{
                             "userId": "7646228072081099729",
                             "userId": "7646228072081099729",
                             "name": "俞玥戎",
                             "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": "俞玥戎",
                             "enName": "俞玥戎",
                             "notify": False
                             "notify": False
                         }]
                         }]
@@ -349,23 +620,44 @@ class FeishuSubmitApp:
                 } if market_person else {"type": 11},
                 } if market_person else {"type": 11},
                 "flde7thxT7": {"type": 3, "value": "optKdksJ5a"},
                 "flde7thxT7": {"type": 3, "value": "optKdksJ5a"},
                 "fldaxavu52": {"type": 3, "value": "optHsn4zKU"},
                 "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 = {
             body = {
@@ -377,12 +669,13 @@ class FeishuSubmitApp:
             return body
             return body
         except Exception as e:
         except Exception as e:
             self.log(f"构建请求体失败: {str(e)}", "error")
             self.log(f"构建请求体失败: {str(e)}", "error")
+            import traceback
+            self.log(f"详细错误: {traceback.format_exc()}", "debug")
             return None
             return None
     
     
     def send_request(self, row_data, row_index):
     def send_request(self, row_data, row_index):
         """发送单条请求"""
         """发送单条请求"""
         try:
         try:
-            # 检查是否有图片字段
             self.log(f"\n{'='*50}", "info")
             self.log(f"\n{'='*50}", "info")
             self.log(f"📤 准备发送第 {row_index} 行数据", "info")
             self.log(f"📤 准备发送第 {row_index} 行数据", "info")
             
             
@@ -392,7 +685,7 @@ class FeishuSubmitApp:
                 return False, "Cookie为空"
                 return False, "Cookie为空"
             
             
             # 构建请求体
             # 构建请求体
-            body = self.build_request_body(row_data)
+            body = self.build_request_body(row_data, row_index)
             if body is None:
             if body is None:
                 self.log("❌ 构建请求体失败", "error")
                 self.log("❌ 构建请求体失败", "error")
                 return False, "构建请求体失败"
                 return False, "构建请求体失败"
@@ -401,20 +694,14 @@ class FeishuSubmitApp:
             body_str = json.dumps(body, ensure_ascii=False)
             body_str = json.dumps(body, ensure_ascii=False)
             self.log(f"📦 请求体大小: {len(body_str)} 字节", "debug")
             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()
             headers = self.default_headers.copy()
-            # 更新Cookie
             cookies = self.parse_cookie(cookie_string)
             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")
             self.log(f"🌐 发送请求到: {self.url}", "debug")
             
             
             # 发送请求
             # 发送请求
@@ -426,16 +713,13 @@ class FeishuSubmitApp:
                 timeout=30
                 timeout=30
             )
             )
             
             
-            # 记录响应状态
             self.log(f"📡 响应状态码: {response.status_code}", "debug")
             self.log(f"📡 响应状态码: {response.status_code}", "debug")
-            self.log(f"📋 响应头: {dict(response.headers)}", "debug")
             
             
             # 解析响应
             # 解析响应
             try:
             try:
                 resp_data = response.json()
                 resp_data = response.json()
                 resp_text = json.dumps(resp_data, ensure_ascii=False, indent=2)
                 resp_text = json.dumps(resp_data, ensure_ascii=False, indent=2)
                 
                 
-                # 检查响应中是否有错误信息
                 if isinstance(resp_data, dict):
                 if isinstance(resp_data, dict):
                     if resp_data.get('code') != 0 and resp_data.get('code') is not None:
                     if resp_data.get('code') != 0 and resp_data.get('code') is not None:
                         self.log(f"⚠️ 服务器返回错误码: {resp_data.get('code')}", "warning")
                         self.log(f"⚠️ 服务器返回错误码: {resp_data.get('code')}", "warning")
@@ -448,17 +732,6 @@ class FeishuSubmitApp:
             
             
             if response.status_code == 200:
             if response.status_code == 200:
                 self.log(f"✅ 第{row_index}行 发送成功", "success")
                 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
                 return True, resp_text
             else:
             else:
                 self.log(f"❌ 第{row_index}行 发送失败 (HTTP {response.status_code})", "error")
                 self.log(f"❌ 第{row_index}行 发送失败 (HTTP {response.status_code})", "error")
@@ -473,6 +746,8 @@ class FeishuSubmitApp:
             return False, "连接失败"
             return False, "连接失败"
         except Exception as e:
         except Exception as e:
             self.log(f"💥 第{row_index}行 异常: {str(e)}", "error")
             self.log(f"💥 第{row_index}行 异常: {str(e)}", "error")
+            import traceback
+            self.log(f"详细错误: {traceback.format_exc()}", "debug")
             return False, str(e)
             return False, str(e)
         finally:
         finally:
             self.log(f"{'='*50}\n", "info")
             self.log(f"{'='*50}\n", "info")
@@ -480,43 +755,40 @@ class FeishuSubmitApp:
     def send_all(self):
     def send_all(self):
         """发送所有数据的主循环"""
         """发送所有数据的主循环"""
         try:
         try:
-            # 获取配置
             excel_file = self.file_path_var.get()
             excel_file = self.file_path_var.get()
             sheet_name = self.sheet_var.get()
             sheet_name = self.sheet_var.get()
             min_interval = float(self.min_interval_var.get())
             min_interval = float(self.min_interval_var.get())
             max_interval = float(self.max_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:
             if not excel_file:
                 self.log("❌ 请先选择Excel文件", "error")
                 self.log("❌ 请先选择Excel文件", "error")
                 self.status_var.set("❌ 请先选择Excel文件")
                 self.status_var.set("❌ 请先选择Excel文件")
                 return
                 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:
             try:
                 df = pd.read_excel(excel_file, sheet_name=sheet_name)
                 df = pd.read_excel(excel_file, sheet_name=sheet_name)
                 self.log(f"📊 成功读取Excel: {len(df)} 行数据", "info")
                 self.log(f"📊 成功读取Excel: {len(df)} 行数据", "info")
                 self.log(f"📋 列名: {', '.join(df.columns.tolist())}", "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:
             except Exception as e:
                 self.log(f"❌ 读取Excel失败: {str(e)}", "error")
                 self.log(f"❌ 读取Excel失败: {str(e)}", "error")
                 self.status_var.set(f"❌ 读取失败: {str(e)}")
                 self.status_var.set(f"❌ 读取失败: {str(e)}")
@@ -527,7 +799,6 @@ class FeishuSubmitApp:
                 self.status_var.set("⚠️ 没有数据")
                 self.status_var.set("⚠️ 没有数据")
                 return
                 return
             
             
-            # 从指定行开始
             df = df.iloc[start_row:]
             df = df.iloc[start_row:]
             total = len(df)
             total = len(df)
             success_count = 0
             success_count = 0
@@ -542,30 +813,38 @@ class FeishuSubmitApp:
                     self.status_var.set("⏹️ 已停止")
                     self.status_var.set("⏹️ 已停止")
                     break
                     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:
                 if success:
                     success_count += 1
                     success_count += 1
                 else:
                 else:
                     fail_count += 1
                     fail_count += 1
                 
                 
-                # 更新状态
                 self.status_var.set(f"🔄 发送中... {idx+1}/{total} (成功:{success_count} 失败:{fail_count})")
                 self.status_var.set(f"🔄 发送中... {idx+1}/{total} (成功:{success_count} 失败:{fail_count})")
                 
                 
-                # 随机间隔(最后一条不等待)
                 if idx < total - 1 and not self.stop_flag:
                 if idx < total - 1 and not self.stop_flag:
                     interval = random.uniform(min_interval, max_interval)
                     interval = random.uniform(min_interval, max_interval)
                     self.log(f"⏳ 等待 {interval:.1f} 秒...", "info")
                     self.log(f"⏳ 等待 {interval:.1f} 秒...", "info")
                     time.sleep(interval)
                     time.sleep(interval)
             
             
-            # 完成统计
             self.log(f"🏁 发送完成!成功: {success_count}, 失败: {fail_count}", "success" if fail_count == 0 else "warning")
             self.log(f"🏁 发送完成!成功: {success_count}, 失败: {fail_count}", "success" if fail_count == 0 else "warning")
             self.status_var.set(f"✅ 完成 成功:{success_count} 失败:{fail_count}")
             self.status_var.set(f"✅ 完成 成功:{success_count} 失败:{fail_count}")
             
             
         except Exception as e:
         except Exception as e:
             self.log(f"💥 程序异常: {str(e)}", "error")
             self.log(f"💥 程序异常: {str(e)}", "error")
+            import traceback
+            self.log(f"详细错误: {traceback.format_exc()}", "debug")
             self.status_var.set(f"❌ 异常: {str(e)}")
             self.status_var.set(f"❌ 异常: {str(e)}")
         finally:
         finally:
             self.is_running = False
             self.is_running = False
@@ -577,7 +856,6 @@ class FeishuSubmitApp:
         if self.is_running:
         if self.is_running:
             return
             return
         
         
-        # 验证配置
         if not self.file_path_var.get():
         if not self.file_path_var.get():
             messagebox.showwarning("提示", "请先选择Excel文件")
             messagebox.showwarning("提示", "请先选择Excel文件")
             return
             return
@@ -595,14 +873,12 @@ class FeishuSubmitApp:
             messagebox.showerror("错误", "间隔或行号请输入有效数字")
             messagebox.showerror("错误", "间隔或行号请输入有效数字")
             return
             return
         
         
-        # 重置状态
         self.is_running = True
         self.is_running = True
         self.stop_flag = False
         self.stop_flag = False
         self.start_btn.config(state=tk.DISABLED)
         self.start_btn.config(state=tk.DISABLED)
         self.stop_btn.config(state=tk.NORMAL)
         self.stop_btn.config(state=tk.NORMAL)
         self.status_var.set("🔄 正在发送...")
         self.status_var.set("🔄 正在发送...")
         
         
-        # 启动线程
         thread = threading.Thread(target=self.send_all, daemon=True)
         thread = threading.Thread(target=self.send_all, daemon=True)
         thread.start()
         thread.start()
     
     
@@ -612,6 +888,147 @@ class FeishuSubmitApp:
         self.log("⏹️ 正在停止...", "warning")
         self.log("⏹️ 正在停止...", "warning")
         self.stop_btn.config(state=tk.DISABLED)
         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__":
 if __name__ == "__main__":
     root = tk.Tk()
     root = tk.Tk()

Some files were not shown because too many files changed in this diff