Browse Source

Merge branch 'main' of https://git.port.run/puge/pythonTest

PUGE 1 tháng trước cách đây
mục cha
commit
ab5d72cc0d
3 tập tin đã thay đổi với 747 bổ sung và 189 xóa
  1. 280 160
      87总控.py
  2. 36 3
      test2.py
  3. 431 26
      自动匹配上架图片.py

+ 280 - 160
87总控.py

@@ -29,12 +29,12 @@ class WindowManagerApp(QMainWindow):
         self.is_running = False
         self.should_stop = False  # 停止标志
         self.initUI()
-        
+
     def initUI(self):
         """初始化界面"""
         self.setWindowTitle('霸器窗口自动点击工具 - 多窗口批量执行')
-        self.setGeometry(100, 100, 950, 750)
-        
+        self.setGeometry(100, 100, 950, 800)
+
         # 设置主窗口样式
         self.setStyleSheet("""
             QMainWindow {
@@ -92,6 +92,28 @@ class WindowManagerApp(QMainWindow):
             QPushButton#stopBtn:hover {
                 background-color: #e14448;
             }
+            QPushButton#funcBtn {
+                background-color: #8b5cf6;
+                font-size: 13px;
+                padding: 10px 20px;
+            }
+            QPushButton#funcBtn:hover {
+                background-color: #7c3aed;
+            }
+            QPushButton#funcBtn:disabled {
+                background-color: #555;
+            }
+            QPushButton#palaceBtn {
+                background-color: #f59e0b;
+                font-size: 13px;
+                padding: 10px 20px;
+            }
+            QPushButton#palaceBtn:hover {
+                background-color: #d97706;
+            }
+            QPushButton#palaceBtn:disabled {
+                background-color: #555;
+            }
             QListWidget {
                 background-color: #3c3c3c;
                 color: #ffffff;
@@ -163,12 +185,12 @@ class WindowManagerApp(QMainWindow):
                 font-size: 10pt;
             }
         """)
-        
+
         # 创建中央窗口
         central_widget = QWidget()
         self.setCentralWidget(central_widget)
         main_layout = QVBoxLayout(central_widget)
-        
+
         # 标题
         title_label = QLabel('霸器窗口自动点击工具 - 批量执行')
         title_label.setAlignment(Qt.AlignCenter)
@@ -176,158 +198,188 @@ class WindowManagerApp(QMainWindow):
         title_label.setFont(title_font)
         title_label.setStyleSheet('color: #00b4d8; margin: 15px;')
         main_layout.addWidget(title_label)
-        
+
         # 控制区域
         control_group = QGroupBox('控制面板')
         control_layout = QHBoxLayout()
-        
+
         self.refresh_btn = QPushButton('🔄 加载窗口')
         self.refresh_btn.setObjectName('refreshBtn')
         self.refresh_btn.clicked.connect(self.load_processes)
         control_layout.addWidget(self.refresh_btn)
-        
+
         self.filter_label = QLabel('筛选:')
         control_layout.addWidget(self.filter_label)
-        
+
         self.filter_input = QLineEdit()
         self.filter_input.setPlaceholderText('输入标题关键词过滤...')
         self.filter_input.textChanged.connect(self.filter_list)
         control_layout.addWidget(self.filter_input)
-        
+
         # 显示模式选择
         self.show_mode_combo = QComboBox()
         self.show_mode_combo.addItems(['所有窗口', '仅可见窗口', '仅隐藏窗口'])
         self.show_mode_combo.currentTextChanged.connect(self.on_show_mode_changed)
         control_layout.addWidget(QLabel('显示:'))
         control_layout.addWidget(self.show_mode_combo)
-        
+
         control_group.setLayout(control_layout)
         main_layout.addWidget(control_group)
-        
-        # 复选框区域 - 功能选择
-        checkbox_group = QGroupBox('功能选择')
+
+        # ==================== 功能选择区域 ====================
+        func_group = QGroupBox('功能选择 (联赛相关)')
+        func_main_layout = QVBoxLayout()
+
+        # 复选框
         checkbox_layout = QHBoxLayout()
-        
+
         self.cb_molong = QCheckBox('争夺魔龙之心')
         self.cb_molong.setChecked(False)
         checkbox_layout.addWidget(self.cb_molong)
-        
+
         self.cb_feicui = QCheckBox('翡翠荣耀旗')
         self.cb_feicui.setChecked(False)
         checkbox_layout.addWidget(self.cb_feicui)
-        
+
         self.cb_jinyan = QCheckBox('金岩霸者棋')
         self.cb_jinyan.setChecked(False)
         checkbox_layout.addWidget(self.cb_jinyan)
-        
+
         self.cb_chiyan = QCheckBox('炽焰战神棋')
         self.cb_chiyan.setChecked(False)
         checkbox_layout.addWidget(self.cb_chiyan)
-        
-        checkbox_group.setLayout(checkbox_layout)
-        main_layout.addWidget(checkbox_group)
-        
-        # 单选按钮区域 - 皇宫占位
-        radio_group = QGroupBox('皇宫占位位置')
+
+        checkbox_layout.addStretch()
+        func_main_layout.addLayout(checkbox_layout)
+
+        # 功能选择执行按钮
+        func_btn_layout = QHBoxLayout()
+        self.func_btn = QPushButton('⚡ 单独执行功能选择')
+        self.func_btn.setObjectName('funcBtn')
+        self.func_btn.clicked.connect(lambda: self.start_batch('function'))
+        func_btn_layout.addWidget(self.func_btn)
+        func_btn_layout.addStretch()
+        func_main_layout.addLayout(func_btn_layout)
+
+        func_group.setLayout(func_main_layout)
+        main_layout.addWidget(func_group)
+
+        # ==================== 皇宫占位区域 ====================
+        palace_group = QGroupBox('皇宫占位位置')
+        palace_main_layout = QVBoxLayout()
+
+        # 单选按钮
         radio_layout = QHBoxLayout()
-        
+
         self.radio_random = QRadioButton('随机')
         self.radio_random.setChecked(True)
         radio_layout.addWidget(self.radio_random)
-        
+
         self.radio_door = QRadioButton('门口')
         radio_layout.addWidget(self.radio_door)
-        
+
         self.radio_corner = QRadioButton('角落')
         radio_layout.addWidget(self.radio_corner)
-        
+
         # 将单选按钮分组
         self.radio_button_group = QButtonGroup()
         self.radio_button_group.addButton(self.radio_random)
         self.radio_button_group.addButton(self.radio_door)
         self.radio_button_group.addButton(self.radio_corner)
-        
-        radio_group.setLayout(radio_layout)
-        main_layout.addWidget(radio_group)
-        
+
+        radio_layout.addStretch()
+        palace_main_layout.addLayout(radio_layout)
+
+        # 皇宫占位执行按钮
+        palace_btn_layout = QHBoxLayout()
+        self.palace_btn = QPushButton('👑 单独执行皇宫占位')
+        self.palace_btn.setObjectName('palaceBtn')
+        self.palace_btn.clicked.connect(lambda: self.start_batch('palace'))
+        palace_btn_layout.addWidget(self.palace_btn)
+        palace_btn_layout.addStretch()
+        palace_main_layout.addLayout(palace_btn_layout)
+
+        palace_group.setLayout(palace_main_layout)
+        main_layout.addWidget(palace_group)
+
         # 窗口列表 - 支持多选
         list_group = QGroupBox('窗口列表 (霸器-开头) - 可多选')
         list_layout = QVBoxLayout()
-        
+
         self.window_list = QListWidget()
         self.window_list.setSelectionMode(QListWidget.MultiSelection)  # 改为多选模式
         self.window_list.itemDoubleClicked.connect(self.show_selected_window)
         list_layout.addWidget(self.window_list)
-        
+
         # 添加全选/取消全选按钮
         select_layout = QHBoxLayout()
         self.select_all_btn = QPushButton('全选')
         self.select_all_btn.clicked.connect(self.select_all_items)
         select_layout.addWidget(self.select_all_btn)
-        
+
         self.deselect_all_btn = QPushButton('取消全选')
         self.deselect_all_btn.clicked.connect(self.deselect_all_items)
         select_layout.addWidget(self.deselect_all_btn)
-        
+
         self.selected_count_label = QLabel('已选: 0 个窗口')
         self.selected_count_label.setStyleSheet('color: #00b4d8;')
         select_layout.addWidget(self.selected_count_label)
         select_layout.addStretch()
-        
+
         list_layout.addLayout(select_layout)
-        
+
         list_group.setLayout(list_layout)
         main_layout.addWidget(list_group)
-        
+
         # 操作按钮区域
-        action_group = QGroupBox('操作')
+        action_group = QGroupBox('批量操作')
         action_layout = QHBoxLayout()
-        
-        self.start_btn = QPushButton('▶️ 开始批量执行')
+
+        self.start_btn = QPushButton('▶️ 全部执行 (功能+皇宫)')
         self.start_btn.setObjectName('startBtn')
-        self.start_btn.clicked.connect(self.start_auto_click)
+        self.start_btn.clicked.connect(lambda: self.start_batch('all'))
         action_layout.addWidget(self.start_btn)
-        
+
         self.stop_btn = QPushButton('⏹️ 停止')
         self.stop_btn.setObjectName('stopBtn')
         self.stop_btn.clicked.connect(self.stop_auto_click)
         self.stop_btn.setEnabled(False)
         action_layout.addWidget(self.stop_btn)
-        
+
         self.show_btn = QPushButton('👁️ 显示选中窗口')
         self.show_btn.setObjectName('showBtn')
         self.show_btn.clicked.connect(self.show_selected_window)
         action_layout.addWidget(self.show_btn)
-        
+
         action_group.setLayout(action_layout)
         main_layout.addWidget(action_group)
-        
+
         # 日志区域
         log_group = QGroupBox('运行日志')
         log_layout = QVBoxLayout()
-        
+
         self.log_text = QTextEdit()
         self.log_text.setReadOnly(True)
         self.log_text.setMaximumHeight(150)
         log_layout.addWidget(self.log_text)
-        
+
         log_group.setLayout(log_layout)
         main_layout.addWidget(log_group)
-        
+
         # 状态栏
         self.status_label = QLabel('就绪')
         self.status_label.setStyleSheet('color: #888; margin: 5px;')
         main_layout.addWidget(self.status_label)
-        
+
         # 自动加载
         self.load_processes()
-        
+
         # 工作线程信号
         self.signals = WorkerSignals()
         self.signals.status.connect(self.update_status)
         self.signals.progress.connect(self.update_progress)
         self.signals.finished.connect(self.on_finished)
-    
+
     def select_all_items(self):
         """全选所有窗口"""
         for i in range(self.window_list.count()):
@@ -335,31 +387,31 @@ class WindowManagerApp(QMainWindow):
             if not item.isHidden():
                 item.setSelected(True)
         self.update_selected_count()
-    
+
     def deselect_all_items(self):
         """取消全选"""
         self.window_list.clearSelection()
         self.update_selected_count()
-    
+
     def update_selected_count(self):
         """更新已选数量"""
         count = len(self.window_list.selectedItems())
         self.selected_count_label.setText(f'已选: {count} 个窗口')
-    
+
     def get_window_title(self, hwnd):
         """获取窗口标题"""
         try:
             return win32gui.GetWindowText(hwnd)
         except:
             return ''
-    
+
     def is_window_visible(self, hwnd):
         """检查窗口是否可见"""
         try:
             return win32gui.IsWindowVisible(hwnd)
         except:
             return False
-    
+
     def enum_all_windows_callback(self, hwnd, windows_list):
         """枚举所有窗口的回调函数(包括隐藏窗口)"""
         if win32gui.IsWindow(hwnd):
@@ -367,30 +419,30 @@ class WindowManagerApp(QMainWindow):
             if title:  # 只记录有标题的窗口
                 windows_list.append((hwnd, title))
         return True
-    
+
     def load_processes(self):
         """加载所有进程窗口(包括隐藏的)"""
         try:
             self.window_list.clear()
             self.window_handles = []
             self.all_windows = []
-            
+
             windows_list = []
             win32gui.EnumWindows(self.enum_all_windows_callback, windows_list)
-            
+
             self.all_windows = windows_list.copy()
-            
+
             # 筛选以"霸器-"开头的窗口
             baqi_windows = [(hwnd, title) for hwnd, title in windows_list if title.startswith('霸器-')]
-            
+
             if not baqi_windows:
                 self.status_label.setText(f'未找到以"霸器-"开头的窗口 (共找到 {len(windows_list)} 个窗口)')
                 self.log_text.append(f'⚠️ 未找到霸器窗口,共找到 {len(windows_list)} 个窗口')
                 return
-            
+
             # 按标题排序
             baqi_windows.sort(key=lambda x: x[1])
-            
+
             for hwnd, title in baqi_windows:
                 item = QListWidgetItem(title)
                 item.setData(Qt.UserRole, hwnd)
@@ -400,29 +452,29 @@ class WindowManagerApp(QMainWindow):
                 item.setData(Qt.UserRole + 1, visible)
                 self.window_list.addItem(item)
                 self.window_handles.append(hwnd)
-            
+
             visible_count = sum(1 for _, title in baqi_windows if self.is_window_visible(_))
             hidden_count = len(baqi_windows) - visible_count
-            
+
             self.status_label.setText(f'找到 {len(baqi_windows)} 个"霸器-"窗口 (可见: {visible_count}, 隐藏: {hidden_count})')
             self.log_text.append(f'✅ 加载完成,找到 {len(baqi_windows)} 个霸器窗口')
-            
+
             # 应用显示模式过滤
             self.apply_show_mode()
             self.update_selected_count()
-            
+
         except Exception as e:
             QMessageBox.critical(self, '错误', f'加载进程失败: {str(e)}')
             self.status_label.setText('加载失败')
             self.log_text.append(f'❌ 加载失败: {str(e)}')
-    
+
     def apply_show_mode(self):
         """应用显示模式过滤"""
         mode = self.show_mode_combo.currentText()
         for i in range(self.window_list.count()):
             item = self.window_list.item(i)
             visible = item.data(Qt.UserRole + 1)
-            
+
             if mode == '所有窗口':
                 item.setHidden(False)
             elif mode == '仅可见窗口':
@@ -430,11 +482,11 @@ class WindowManagerApp(QMainWindow):
             elif mode == '仅隐藏窗口':
                 item.setHidden(visible)
         self.update_selected_count()
-    
+
     def on_show_mode_changed(self):
         """显示模式改变时重新过滤"""
         self.apply_show_mode()
-    
+
     def filter_list(self):
         """根据输入过滤列表"""
         filter_text = self.filter_input.text().lower()
@@ -446,7 +498,7 @@ class WindowManagerApp(QMainWindow):
             else:
                 item.setHidden(True)
         self.apply_show_mode()
-    
+
     def get_selected_windows(self):
         """获取所有选中的窗口"""
         selected_items = self.window_list.selectedItems()
@@ -456,28 +508,28 @@ class WindowManagerApp(QMainWindow):
             title = item.text().replace(' ✓', '').replace(' ✗', '')
             windows.append((hwnd, title))
         return windows
-    
+
     def show_selected_window(self):
         """显示选中的窗口(只显示第一个选中的)"""
         selected = self.get_selected_windows()
         if not selected:
             QMessageBox.warning(self, '警告', '请先选择窗口')
             return
-        
+
         hwnd, title = selected[0]
         try:
             win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
             win32gui.SetForegroundWindow(hwnd)
             if win32gui.IsIconic(hwnd):
                 win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
-            
+
             self.status_label.setText(f'已显示窗口: {title}')
             self.log_text.append(f'👁️ 显示窗口: {title}')
             self.refresh_window_visibility()
-            
+
         except Exception as e:
             QMessageBox.critical(self, '错误', f'显示窗口失败: {str(e)}')
-    
+
     def refresh_window_visibility(self):
         """刷新窗口中显示的可见状态"""
         for i in range(self.window_list.count()):
@@ -488,9 +540,9 @@ class WindowManagerApp(QMainWindow):
             status_text = " ✓" if visible else " ✗"
             item.setText(title + status_text)
             item.setData(Qt.UserRole + 1, visible)
-        
+
         self.apply_show_mode()
-    
+
     def minimize_window(self, hwnd):
         """最小化窗口"""
         try:
@@ -500,7 +552,7 @@ class WindowManagerApp(QMainWindow):
         except Exception as e:
             self.signals.status.emit(f'❌ 最小化失败: {str(e)}')
             return False
-    
+
     def click_at_position(self, hwnd, x_offset, y_offset, button='left'):
         """
         封装的点击函数
@@ -513,15 +565,15 @@ class WindowManagerApp(QMainWindow):
         rect = win32gui.GetWindowRect(hwnd)
         window_x = rect[0]
         window_y = rect[1]
-        
+
         # 计算点击位置
         click_x = window_x + x_offset
         click_y = window_y + y_offset
-        
+
         # 移动鼠标到目标位置
         win32api.SetCursorPos((click_x, click_y))
         time.sleep(0.2)
-        
+
         # 模拟鼠标点击
         if button == 'left':
             win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, click_x, click_y, 0, 0)
@@ -533,42 +585,42 @@ class WindowManagerApp(QMainWindow):
             time.sleep(0.1)
             win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTUP, click_x, click_y, 0, 0)
             self.signals.status.emit(f'🖱️ 右键点击: ({click_x}, {click_y})')
-        
+
         return click_x, click_y
-    
+
     def get_pixel_color(self, hwnd, x_offset, y_offset):
         try:
             # 获取窗口位置
             rect = win32gui.GetWindowRect(hwnd)
             window_x = rect[0]
             window_y = rect[1]
-            
+
             # 计算屏幕坐标
             screen_x = window_x + x_offset
             screen_y = window_y + y_offset
-            
+
             # 获取桌面DC
             hdc = win32gui.GetDC(0)  # 0 表示桌面
-            
+
             color_ref = win32gui.GetPixel(hdc, screen_x, screen_y)
-            
+
             win32gui.ReleaseDC(0, hdc)
-            
+
             red = color_ref & 0xFF
             green = (color_ref >> 8) & 0xFF
             blue = (color_ref >> 16) & 0xFF
-            
+
             return f"{red:02X}{green:02X}{blue:02X}"
-            
+
         except Exception as e:
             self.log_text.append(f'❌ 取色失败: {str(e)}')
             return "000000"
-    
+
     def check_and_click(self, hwnd, check_x, check_y, target_color, 
                         click_x, click_y, button='left', check_type='equal'):
         """
         检查颜色并执行点击
-        
+
         :param hwnd: 窗口句柄
         :param check_x: 检查颜色的X偏移
         :param check_y: 检查颜色的Y偏移
@@ -581,15 +633,15 @@ class WindowManagerApp(QMainWindow):
         """
         # 获取当前颜色
         current_color = self.get_pixel_color(hwnd, check_x, check_y)
-        
+
         self.log_text.append(f'🎨 检测位置({check_x},{check_y})颜色: {current_color} (目标: {target_color})')
-        
+
         should_click = False
         if check_type == 'equal':
             should_click = (current_color == target_color)
         elif check_type == 'not_equal':
             should_click = (current_color != target_color)
-        
+
         if should_click:
             self.log_text.append(f'✅ 条件满足,执行点击 ({click_x}, {click_y})')
             self.click_at_position(hwnd, click_x, click_y, button)
@@ -597,36 +649,47 @@ class WindowManagerApp(QMainWindow):
         else:
             self.log_text.append(f'❌ 条件不满足,跳过点击')
             return False
-    
-    def start_auto_click(self):
-        """开始自动点击 - 批量执行"""
+
+    def start_batch(self, mode):
+        """开始批量执行 - 支持单独模式
+        mode: 'all' | 'function' | 'palace'
+        """
         if self.is_running:
             return
-        
+
         # 获取所有选中的窗口
         selected_windows = self.get_selected_windows()
         if not selected_windows:
             QMessageBox.warning(self, '警告', '请至少选择一个窗口')
             return
-        
-        # 检查是否至少选择了一个功能
-        if not (self.cb_molong.isChecked() or self.cb_feicui.isChecked() or 
-                self.cb_jinyan.isChecked() or self.cb_chiyan.isChecked()):
-            QMessageBox.warning(self, '警告', '请至少选择一个功能')
-            return
-        
+
+        # 根据模式验证
+        if mode == 'function':
+            if not (self.cb_molong.isChecked() or self.cb_feicui.isChecked() or 
+                    self.cb_jinyan.isChecked() or self.cb_chiyan.isChecked()):
+                QMessageBox.warning(self, '警告', '请至少选择一个功能')
+                return
+        elif mode == 'all':
+            if not (self.cb_molong.isChecked() or self.cb_feicui.isChecked() or 
+                    self.cb_jinyan.isChecked() or self.cb_chiyan.isChecked()):
+                QMessageBox.warning(self, '警告', '请至少选择一个功能,或单独执行皇宫占位')
+                return
+        # palace 模式不需要额外验证
+
         self.is_running = True
         self.should_stop = False
         self.start_btn.setEnabled(False)
+        self.func_btn.setEnabled(False)
+        self.palace_btn.setEnabled(False)
         self.stop_btn.setEnabled(True)
-        
+
         # 记录选择的选项
         selected = []
         if self.cb_molong.isChecked(): selected.append('争夺魔龙之心')
         if self.cb_feicui.isChecked(): selected.append('翡翠荣耀旗')
         if self.cb_jinyan.isChecked(): selected.append('金岩霸者棋')
         if self.cb_chiyan.isChecked(): selected.append('炽焰战神棋')
-        
+
         # 记录皇宫占位选择
         if self.radio_random.isChecked():
             palace_pos = '随机'
@@ -637,90 +700,143 @@ class WindowManagerApp(QMainWindow):
         else:  # corner
             palace_pos = '角落'
             palace_y = 470
-        
-        self.log_text.append(f'▶️ 开始批量执行,共 {len(selected_windows)} 个窗口')
-        self.log_text.append(f'📋 已选功能: {", ".join(selected)}')
-        self.log_text.append(f'🏰 皇宫占位: {palace_pos} (Y+{palace_y})')
+
+        # 根据模式显示不同的日志
+        if mode == 'all':
+            self.log_text.append(f'▶️ 开始【全部执行】,共 {len(selected_windows)} 个窗口')
+            self.log_text.append(f'📋 已选功能: {", ".join(selected) if selected else "无"}')
+            self.log_text.append(f'🏰 皇宫占位: {palace_pos} (Y+{palace_y})')
+        elif mode == 'function':
+            self.log_text.append(f'▶️ 开始【单独执行功能选择】,共 {len(selected_windows)} 个窗口')
+            self.log_text.append(f'📋 已选功能: {", ".join(selected)}')
+        elif mode == 'palace':
+            self.log_text.append(f'▶️ 开始【单独执行皇宫占位】,共 {len(selected_windows)} 个窗口')
+            self.log_text.append(f'🏰 皇宫占位: {palace_pos} (Y+{palace_y})')
+
         self.log_text.append('=' * 50)
-        
+
         # 在新线程中执行
         thread = threading.Thread(target=self.batch_auto_click_worker, 
-                                 args=(selected_windows, palace_y))
+                                 args=(selected_windows, palace_y, mode))
         thread.daemon = True
         thread.start()
-    
-    def batch_auto_click_worker(self, selected_windows, palace_y):
-        """批量自动点击工作线程"""
+
+    def batch_auto_click_worker(self, selected_windows, palace_y, mode):
+        """批量自动点击工作线程
+        mode: 'all' | 'function' | 'palace'
+        """
         total = len(selected_windows)
-        
+
         for idx, (hwnd, title) in enumerate(selected_windows, 1):
             # 检查是否被停止
             if self.should_stop:
                 self.signals.status.emit(f'⏹️ 用户停止执行')
                 break
-            
+
             self.signals.status.emit(f'📌 正在处理 [{idx}/{total}] {title}')
             self.signals.progress.emit(idx, total)
-            
+
             try:
-                # 执行单个窗口的点击流程
-                self.execute_single_window(hwnd, title, palace_y)
-                
+                # 根据模式执行不同的流程
+                if mode == 'all':
+                    # 全部执行:功能 + 皇宫
+                    self.execute_single_window_all(hwnd, title, palace_y)
+                elif mode == 'function':
+                    # 只执行功能选择
+                    self.execute_single_window_function(hwnd, title)
+                elif mode == 'palace':
+                    # 只执行皇宫占位
+                    self.execute_single_window_palace(hwnd, title, palace_y)
+
                 # 等待5秒后最小化窗口
                 self.signals.status.emit(f'⏳ 等待5秒后最小化窗口...')
                 time.sleep(5)
-                
+
                 # 最小化窗口
                 self.signals.status.emit(f'📉 最小化窗口: {title}')
                 self.minimize_window(hwnd)
-                
+
                 # 窗口间间隔1秒
                 if idx < total and not self.should_stop:
                     self.signals.status.emit(f'⏳ 等待1秒后处理下一个窗口...')
                     time.sleep(1)
-                    
+
             except Exception as e:
                 self.signals.status.emit(f'❌ 处理窗口失败: {str(e)}')
                 import traceback
                 self.log_text.append(traceback.format_exc())
                 # 出错后继续下一个窗口
-        
+
         if not self.should_stop:
             self.signals.status.emit(f'✅ 批量执行完成!共处理 {idx if idx < total else total} 个窗口')
         self.signals.finished.emit()
-    
-    def execute_single_window(self, hwnd, title, palace_y):
-        """执行单个窗口的点击流程"""
-        # 1. 激活窗口
+
+    def activate_window(self, hwnd, title):
+        """激活窗口"""
         self.signals.status.emit(f'🔄 正在激活窗口: {title}')
-        
+
         # 显示窗口
         win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
         if win32gui.IsIconic(hwnd):
             win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
-        
+
         # 激活窗口
         win32gui.SetForegroundWindow(hwnd)
         time.sleep(1)  # 等待窗口激活
-        
+
+    def execute_single_window_all(self, hwnd, title, palace_y):
+        """执行单个窗口的完整流程(功能+皇宫)"""
+        # 1. 激活窗口
+        self.activate_window(hwnd, title)
+
+        # 执行功能选择部分
+        self._do_function_clicks(hwnd)
+
+        # 执行皇宫占位部分
+        self._do_palace_clicks(hwnd, palace_y)
+
+        self.signals.status.emit(f'✅ 窗口 {title} 全部执行完成')
+
+    def execute_single_window_function(self, hwnd, title):
+        """只执行功能选择部分"""
+        # 1. 激活窗口
+        self.activate_window(hwnd, title)
+
+        # 执行功能选择部分
+        self._do_function_clicks(hwnd)
+
+        self.signals.status.emit(f'✅ 窗口 {title} 功能选择执行完成')
+
+    def execute_single_window_palace(self, hwnd, title, palace_y):
+        """只执行皇宫占位部分"""
+        # 1. 激活窗口
+        self.activate_window(hwnd, title)
+
+        # 执行皇宫占位部分
+        self._do_palace_clicks(hwnd, palace_y)
+
+        self.signals.status.emit(f'✅ 窗口 {title} 皇宫占位执行完成')
+
+    def _do_function_clicks(self, hwnd):
+        """功能选择的点击逻辑(可复用)"""
         # 步骤1: 左键点击 X+63, Y+81
         self.click_at_position(hwnd, 29, 41, 'left')
         time.sleep(1)
         self.signals.status.emit(f'📍 步骤1: 左键点击 X+63, Y+81')
         self.click_at_position(hwnd, 63, 81, 'left')
-        
+
         # 等待5秒
         self.signals.status.emit(f'⏳ 等待5秒...')
         time.sleep(5)
-        
+
         # 步骤2: 右键点击 X+413, Y+168
         self.signals.status.emit(f'📍 步骤2: 右键点击 X+413, Y+168')
         self.click_at_position(hwnd, 413, 168, 'right')
-        
+
         # 等待6秒
         self.signals.status.emit(f'⏳ 等待6秒...')
         time.sleep(6)
-        
+
         # 处理"争夺魔龙之心"
         if self.cb_molong.isChecked():
             self.signals.status.emit(f'📍 检查争夺魔龙之心')
@@ -746,10 +862,10 @@ class WindowManagerApp(QMainWindow):
                 button='left',
                 check_type='not_equal'
             )
-        
+
         # 等待1秒
         time.sleep(0.2)
-        
+
         # 处理"翡翠荣耀旗"
         if self.cb_feicui.isChecked():
             self.signals.status.emit(f'📍 检查翡翠荣耀旗')
@@ -775,10 +891,10 @@ class WindowManagerApp(QMainWindow):
                 button='left',
                 check_type='not_equal'
             )
-        
+
         # 等待1秒
         time.sleep(0.2)
-        
+
         # 处理"金岩霸者棋"
         if self.cb_jinyan.isChecked():
             self.signals.status.emit(f'📍 检查金岩霸者棋')
@@ -804,10 +920,10 @@ class WindowManagerApp(QMainWindow):
                 button='left',
                 check_type='not_equal'
             )
-        
+
         # 等待1秒
         time.sleep(0.2)
-        
+
         # 处理"炽焰战神棋"
         if self.cb_chiyan.isChecked():
             self.signals.status.emit(f'📍 检查炽焰战神棋')
@@ -833,14 +949,16 @@ class WindowManagerApp(QMainWindow):
                 button='left',
                 check_type='not_equal'
             )
-        
+
         # 等待1秒
         time.sleep(0.2)
-        
+
         # 最后点击 X+871, Y+266
         self.signals.status.emit(f'📍 最后点击: X+871, Y+266')
         self.click_at_position(hwnd, 871, 266, 'left')
 
+    def _do_palace_clicks(self, hwnd, palace_y):
+        """皇宫占位的点击逻辑(可复用)"""
         time.sleep(0.5)
         self.click_at_position(hwnd, 671, 70, 'right')
 
@@ -848,28 +966,28 @@ class WindowManagerApp(QMainWindow):
         self.click_at_position(hwnd, 714, 426, 'left')
 
         time.sleep(1)
-        
+
         # 皇宫占位点击 - 根据选择的选项
         self.signals.status.emit(f'🏰 皇宫占位点击: X+716, Y+{palace_y}')
         self.click_at_position(hwnd, 716, palace_y, 'left')
-        
+
         time.sleep(1)
         self.click_at_position(hwnd, 901, 311, 'left')
 
         time.sleep(1)
         self.click_at_position(hwnd, 1133, 8, 'left')
-        
-        self.signals.status.emit(f'✅ 窗口 {title} 执行完成')
-    
+
     def stop_auto_click(self):
         """停止自动点击"""
         self.should_stop = True
         self.is_running = False
         self.start_btn.setEnabled(True)
+        self.func_btn.setEnabled(True)
+        self.palace_btn.setEnabled(True)
         self.stop_btn.setEnabled(False)
         self.status_label.setText('正在停止...')
         self.log_text.append('⏹️ 正在停止执行...')
-    
+
     def update_status(self, message):
         """更新状态"""
         self.status_label.setText(message)
@@ -878,15 +996,17 @@ class WindowManagerApp(QMainWindow):
         self.log_text.verticalScrollBar().setValue(
             self.log_text.verticalScrollBar().maximum()
         )
-    
+
     def update_progress(self, current, total):
         """更新进度"""
         self.status_label.setText(f'进度: {current}/{total}')
-    
+
     def on_finished(self):
         """完成回调"""
         self.is_running = False
         self.start_btn.setEnabled(True)
+        self.func_btn.setEnabled(True)
+        self.palace_btn.setEnabled(True)
         self.stop_btn.setEnabled(False)
         self.status_label.setText('就绪')
         self.log_text.append('=' * 50)
@@ -895,10 +1015,10 @@ class WindowManagerApp(QMainWindow):
 def main():
     app = QApplication(sys.argv)
     app.setStyle('Fusion')
-    
+
     window = WindowManagerApp()
     window.show()
-    
+
     sys.exit(app.exec_())
 
 

+ 36 - 3
test2.py

@@ -16,9 +16,9 @@ AWS_SECRET_KEY = "CwBJqlsYATNC920A+U6iv80WnP6Eso+ECfIalQ1W"             # ← 
 AWS_REGION = "us-east-1"
 
 SELLER_ID = "A1H50Z0WYYLEGQ"
-SKU = "sofa table black"
+SKU = "bar table pink"
 MARKETPLACE_ID = "ATVPDKIKX0DER"
-NEW_IMAGE_URL = "https://cunchu.site/temp/f1ae4d03-e59e-45c5-9a3b-15dc3775396a.jpg"
+NEW_IMAGE_URL = "https://cunchu.site/temp/8c7a90e1-a5a1-41af-a119-41b1ca74b40d.png"
 
 # ==================== 工具函数 ====================
 def sha256_hash(data): 
@@ -133,6 +133,39 @@ def patch_image(access_token, image_type, image_url):
     }
     return spapi_request("PATCH", path, query, body=body, access_token=access_token)
 
+def check_submission_result(access_token):
+    encoded_sku = quote(SKU, safe="")
+    path = f"/listings/2021-08-01/items/{SELLER_ID}/{encoded_sku}"
+    # 关键:只带 issues,减少返回数据量
+    query = f"marketplaceIds={MARKETPLACE_ID}&includedData=issues"
+    
+    resp = spapi_request("GET", path, query, access_token=access_token)
+    data = resp.json()
+    
+    issues = data.get("issues", [])
+    if not issues:
+        print("✅ 没有 issues,图片应该已生效")
+        return True
+    
+    print(f"⚠️ 发现 {len(issues)} 个问题:\n")
+    for issue in issues:
+        print(f"  错误码: {issue.get('code')}")
+        print(f"  级别: {issue.get('severity')}")
+        print(f"  信息: {issue.get('message')}")
+        
+        enforcements = issue.get("enforcements", {})
+        actions = enforcements.get("actions", [])
+        for action in actions:
+            print(f"  执行动作: {action.get('action')}")  # 如 ATTRIBUTE_SUPPRESSED
+        
+        # 看是否跟图片相关
+        attrs = issue.get("attributeNames", [])
+        if any("image" in a or "media" in a for a in attrs):
+            print(f"  涉及属性: {attrs} → 图片审核未通过")
+        print()
+    
+    return False
+
 # ==================== 主程序 ====================
 if __name__ == "__main__":
     print("🔑 获取 Access Token...")
@@ -141,7 +174,7 @@ if __name__ == "__main__":
     
     # 示例:增加/修改副图第3张
     new_images = {
-        "other_1": "https://cunchu.site/temp/cd3a846e-f3b4-4858-a643-e1edc60cbe95.png"
+        "other_1": NEW_IMAGE_URL
     }
     
     for img_type, img_url in new_images.items():

+ 431 - 26
自动匹配上架图片.py

@@ -7,13 +7,144 @@ import requests
 from openpyxl import load_workbook
 from openpyxl.utils import get_column_letter
 import re
+import json
+import datetime
+import hashlib
+import hmac
+from urllib.parse import quote
+import time
+
+
+class AmazonImageUploader:
+    """亚马逊图片上传类"""
+    
+    def __init__(self):
+        self.client_id = "amzn1.application-oa2-client.9f775d35077a4c7598116e5a89b60222"
+        self.client_secret = "amzn1.oa2-cs.v1.6909777baa82b796555a431cd23709342e8e01434abcad1f8b59cadf048b51c3"
+        self.refresh_token = "Atzr|IwEBICh_fojh7qAfrePzSjOZhQxTnHRZCYOuWNvSAHGaBs1WfkjR4qjC1Dmv0uK3oyq_L-R9SRKK4cYmPwF1MQXQdqKlpzOAGItWeXnNzh-SyhHHISv0_vCt3D6hluVmIEmF2kA7ro_XF-PslD41g3Eq6bv0Qq4y4pQbkDj-9dudDuz7nku8UL-nc4EWgJCrHCJTAs8TK48Yvrfi3E35uv97tYhs6qb8D9d4dR9C7daaC49eoDqP88qyUHThQV1Kqon8xukx4AT2_wLqWNcabpADzuK-5w7D-vvFFtvzfkbmtfzoPeEdHPulG6scc59BPFTyBlGU7a3d1JSFg2ROHIlrljRm"
+        self.aws_access_key = "AKIA24CLQSVTW2K4S6RP"
+        self.aws_secret_key = "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"
+        self.aws_region = "us-east-1"
+        self.seller_id = "A1H50Z0WYYLEGQ"
+        self.marketplace_id = "ATVPDKIKX0DER"
+        self.product_type = "TABLE"
+        
+    def sha256_hash(self, data):
+        return hashlib.sha256(data.encode("utf-8")).hexdigest()
+    
+    def hmac_sha256(self, key, msg):
+        return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
+    
+    def get_signature_key(self, secret_key, date_stamp, region, service):
+        k_date = self.hmac_sha256(("AWS4" + secret_key).encode("utf-8"), date_stamp)
+        k_region = self.hmac_sha256(k_date, region)
+        k_service = self.hmac_sha256(k_region, service)
+        return self.hmac_sha256(k_service, "aws4_request")
+    
+    def get_access_token(self):
+        url = "https://api.amazon.com/auth/o2/token"
+        resp = requests.post(url, data={
+            "grant_type": "refresh_token",
+            "refresh_token": self.refresh_token,
+            "client_id": self.client_id,
+            "client_secret": self.client_secret,
+        })
+        resp.raise_for_status()
+        return resp.json()["access_token"]
+    
+    def spapi_request(self, method, path, query, body=None, access_token=None):
+        service = "execute-api"
+        host = "sellingpartnerapi-na.amazon.com"
+        payload = json.dumps(body, separators=(",", ":")) if body else ""
+        
+        t = datetime.datetime.now(datetime.timezone.utc)
+        amz_date = t.strftime("%Y%m%dT%H%M%SZ")
+        date_stamp = t.strftime("%Y%m%d")
+        
+        canonical_headers = (
+            f"host:{host}\n"
+            f"x-amz-access-token:{access_token}\n"
+            f"x-amz-date:{amz_date}\n"
+        )
+        signed_headers = "host;x-amz-access-token;x-amz-date"
+        payload_hash = self.sha256_hash(payload)
+        
+        canonical_request = (
+            f"{method}\n{path}\n{query}\n"
+            f"{canonical_headers}\n{signed_headers}\n{payload_hash}"
+        )
+        
+        algorithm = "AWS4-HMAC-SHA256"
+        credential_scope = f"{date_stamp}/{self.aws_region}/{service}/aws4_request"
+        string_to_sign = (
+            f"{algorithm}\n{amz_date}\n{credential_scope}\n"
+            f"{self.sha256_hash(canonical_request)}"
+        )
+        
+        signing_key = self.get_signature_key(self.aws_secret_key, date_stamp, self.aws_region, service)
+        signature = self.hmac_sha256(signing_key, string_to_sign.encode("utf-8")).hexdigest()
+        
+        auth_header = (
+            f"{algorithm} Credential={self.aws_access_key}/{credential_scope}, "
+            f"SignedHeaders={signed_headers}, Signature={signature}"
+        )
+        
+        headers = {
+            "Host": host,
+            "x-amz-access-token": access_token,
+            "x-amz-date": amz_date,
+            "Authorization": auth_header,
+        }
+        if body:
+            headers["Content-Type"] = "application/json"
+        
+        url = f"https://{host}{path}?{query}"
+        return requests.request(method, url, headers=headers, data=payload or None)
+    
+    def patch_image(self, access_token, image_type, image_url, sku):
+        """
+        image_type 可选值:
+          "main"              → 主图
+          "other_1" ~ "other_8" → 副图1~8
+          "swatch"            → 变体图
+        """
+        encoded_sku = quote(sku, safe="")
+        path = f"/listings/2021-08-01/items/{self.seller_id}/{encoded_sku}"
+        query = f"marketplaceIds={self.marketplace_id}"
+        
+        if image_type == "main":
+            attr_path = "/attributes/main_product_image_locator"
+        elif image_type.startswith("other_") and 1 <= int(image_type.split("_")[1]) <= 8:
+            n = image_type.split("_")[1]
+            attr_path = f"/attributes/other_product_image_locator_{n}"
+        elif image_type == "swatch":
+            attr_path = "/attributes/swatch_product_image_locator"
+        else:
+            raise ValueError(f"不支持的 image_type: {image_type}")
+        
+        body = {
+            "productType": self.product_type,
+            "patches": [
+                {
+                    "op": "replace",
+                    "path": attr_path,
+                    "value": [
+                        {
+                            "marketplace_id": self.marketplace_id,
+                            "media_location": image_url
+                        }
+                    ]
+                }
+            ]
+        }
+        return self.spapi_request("PATCH", path, query, body=body, access_token=access_token)
 
 
 class ImageMatcherApp:
     def __init__(self, root):
         self.root = root
-        self.root.title("图片匹配与上传工具")
-        self.root.geometry("850x650")
+        self.root.title("图片匹配与上传工具 - 亚马逊版")
+        self.root.geometry("950x750")
         
         # 存储文件路径
         self.template_path = tk.StringVar()
@@ -24,9 +155,15 @@ class ImageMatcherApp:
         self.upload_group = "soundSynthesis"
         self.upload_user = "1"
         
+        # 亚马逊上传器
+        self.amazon_uploader = AmazonImageUploader()
+        
         # 创建UI
         self.create_widgets()
         
+        # 加载保存的配置
+        self.load_config()
+        
     def create_widgets(self):
         # 主框架
         main_frame = ttk.Frame(self.root, padding="10")
@@ -35,32 +172,58 @@ class ImageMatcherApp:
         # 配置网格权重
         self.root.columnconfigure(0, weight=1)
         self.root.rowconfigure(0, weight=1)
-        main_frame.columnconfigure(1, weight=1)
-        main_frame.rowconfigure(4, weight=1)
+        main_frame.columnconfigure(0, weight=1)
+        main_frame.rowconfigure(1, weight=1)
+        
+        # 创建Notebook(选项卡)
+        self.notebook = ttk.Notebook(main_frame)
+        self.notebook.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
+        
+        # 选项卡1: 图片匹配与上传
+        self.tab_matching = ttk.Frame(self.notebook)
+        self.notebook.add(self.tab_matching, text="图片匹配上传")
+        self.create_matching_tab()
+        
+        # 选项卡2: 亚马逊主图更换
+        self.tab_amazon = ttk.Frame(self.notebook)
+        self.notebook.add(self.tab_amazon, text="亚马逊主图更换")
+        self.create_amazon_tab()
+        
+        # 状态栏
+        self.status_var = tk.StringVar()
+        self.status_var.set("就绪")
+        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
+        status_bar.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
+        
+    def create_matching_tab(self):
+        """创建图片匹配上传选项卡"""
+        tab = self.tab_matching
+        tab.columnconfigure(1, weight=1)
+        tab.rowconfigure(4, weight=1)
         
         # 标题
-        title_label = ttk.Label(main_frame, text="图片匹配与上传工具", font=("Arial", 16, "bold"))
+        title_label = ttk.Label(tab, text="图片匹配与上传", font=("Arial", 14, "bold"))
         title_label.grid(row=0, column=0, columnspan=3, pady=10)
         
         # 模板文件选择
-        ttk.Label(main_frame, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
-        ttk.Entry(main_frame, textvariable=self.template_path, width=50).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
-        ttk.Button(main_frame, text="选择模板", command=self.select_template).grid(row=1, column=2, pady=5, padx=5)
+        ttk.Label(tab, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
+        ttk.Entry(tab, textvariable=self.template_path, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
+        ttk.Button(tab, text="选择模板", command=self.select_template).grid(row=1, column=2, pady=5, padx=5)
         
         # 图片目录选择
-        ttk.Label(main_frame, text="图片目录:").grid(row=2, column=0, sticky=tk.W, pady=5)
-        ttk.Entry(main_frame, textvariable=self.image_dir, width=50).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
-        ttk.Button(main_frame, text="选择目录", command=self.select_image_dir).grid(row=2, column=2, pady=5, padx=5)
+        ttk.Label(tab, text="图片目录:").grid(row=2, column=0, sticky=tk.W, pady=5)
+        ttk.Entry(tab, textvariable=self.image_dir, width=60).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
+        ttk.Button(tab, text="选择目录", command=self.select_image_dir).grid(row=2, column=2, pady=5, padx=5)
         
         # 按钮框架
-        button_frame = ttk.Frame(main_frame)
+        button_frame = ttk.Frame(tab)
         button_frame.grid(row=3, column=0, columnspan=3, pady=10)
         
         ttk.Button(button_frame, text="开始处理", command=self.start_matching, width=15).pack(side=tk.LEFT, padx=5)
         ttk.Button(button_frame, text="清空日志", command=self.clear_log, width=15).pack(side=tk.LEFT, padx=5)
         
         # 日志框
-        log_frame = ttk.LabelFrame(main_frame, text="日志", padding="5")
+        log_frame = ttk.LabelFrame(tab, text="日志", padding="5")
         log_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
         log_frame.columnconfigure(0, weight=1)
         log_frame.rowconfigure(0, weight=1)
@@ -68,15 +231,63 @@ class ImageMatcherApp:
         self.log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
         self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
         
-        # 状态栏
-        self.status_var = tk.StringVar()
-        self.status_var.set("就绪")
-        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
-        status_bar.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
-        
         # 存储生成的文件路径
         self.generated_file_path = None
         
+    def create_amazon_tab(self):
+        """创建亚马逊主图更换选项卡"""
+        tab = self.tab_amazon
+        tab.columnconfigure(1, weight=1)
+        tab.rowconfigure(6, weight=1)
+        
+        # 标题
+        title_label = ttk.Label(tab, text="亚马逊主图/附图批量更换", font=("Arial", 14, "bold"))
+        title_label.grid(row=0, column=0, columnspan=3, pady=10)
+        
+        # 模板文件选择
+        ttk.Label(tab, text="模板文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
+        self.amazon_template_path = tk.StringVar()
+        ttk.Entry(tab, textvariable=self.amazon_template_path, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
+        ttk.Button(tab, text="选择模板", command=self.select_amazon_template).grid(row=1, column=2, pady=5, padx=5)
+        
+        # SKU输入(手动输入,不是从表格读取)
+        ttk.Label(tab, text="SKU (手动输入):").grid(row=2, column=0, sticky=tk.W, pady=5)
+        self.sku_input = tk.StringVar(value="bar grey 03")
+        ttk.Entry(tab, textvariable=self.sku_input, width=40).grid(row=2, column=1, sticky=tk.W, pady=5, padx=5)
+        
+        # 主图列选择
+        ttk.Label(tab, text="主图列名:").grid(row=3, column=0, sticky=tk.W, pady=5)
+        self.main_image_column = tk.StringVar(value="产品主图")
+        ttk.Entry(tab, textvariable=self.main_image_column, width=30).grid(row=3, column=1, sticky=tk.W, pady=5, padx=5)
+        
+        # 附图列选择
+        ttk.Label(tab, text="附图列名(逗号分隔):").grid(row=4, column=0, sticky=tk.W, pady=5)
+        self.other_image_columns = tk.StringVar(value="上架图片1,上架图片2,上架图片3,上架图片4,上架图片5,上架图片6,上架图片7,上架图片8")
+        ttk.Entry(tab, textvariable=self.other_image_columns, width=60).grid(row=4, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
+        
+        # 按钮框架
+        button_frame = ttk.Frame(tab)
+        button_frame.grid(row=5, column=0, columnspan=3, pady=10)
+        
+        ttk.Button(button_frame, text="保存配置", command=self.save_amazon_config, width=12).pack(side=tk.LEFT, padx=5)
+        ttk.Button(button_frame, text="加载配置", command=self.load_amazon_config, width=12).pack(side=tk.LEFT, padx=5)
+        ttk.Button(button_frame, text="开始上传", command=self.start_amazon_upload, width=15).pack(side=tk.LEFT, padx=5)
+        ttk.Button(button_frame, text="清空日志", command=self.clear_amazon_log, width=15).pack(side=tk.LEFT, padx=5)
+        
+        # 日志框
+        log_frame = ttk.LabelFrame(tab, text="上传日志", padding="5")
+        log_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
+        log_frame.columnconfigure(0, weight=1)
+        log_frame.rowconfigure(0, weight=1)
+        
+        self.amazon_log_text = scrolledtext.ScrolledText(log_frame, height=15, width=80, wrap=tk.WORD)
+        self.amazon_log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
+        
+        # 进度标签
+        self.amazon_progress_var = tk.StringVar(value="就绪")
+        progress_label = ttk.Label(tab, textvariable=self.amazon_progress_var, relief=tk.SUNKEN, anchor=tk.W)
+        progress_label.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
+        
     def select_template(self):
         filename = filedialog.askopenfilename(
             title="选择模板文件",
@@ -92,16 +303,87 @@ class ImageMatcherApp:
             self.image_dir.set(directory)
             self.log(f"已选择图片目录: {directory}")
             
+    def select_amazon_template(self):
+        filename = filedialog.askopenfilename(
+            title="选择模板文件",
+            filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
+        )
+        if filename:
+            self.amazon_template_path.set(filename)
+            self.amazon_log(f"已选择模板文件: {filename}")
+            
     def log(self, message):
-        """在日志框中添加消息"""
+        """在匹配选项卡日志框中添加消息"""
         self.log_text.insert(tk.END, f"{message}\n")
         self.log_text.see(tk.END)
         self.root.update_idletasks()
         
     def clear_log(self):
-        """清空日志"""
+        """清空匹配选项卡日志"""
         self.log_text.delete(1.0, tk.END)
         
+    def amazon_log(self, message):
+        """在亚马逊选项卡日志框中添加消息"""
+        self.amazon_log_text.insert(tk.END, f"{message}\n")
+        self.amazon_log_text.see(tk.END)
+        self.root.update_idletasks()
+        
+    def clear_amazon_log(self):
+        """清空亚马逊选项卡日志"""
+        self.amazon_log_text.delete(1.0, tk.END)
+        
+    def get_config_path(self):
+        """获取配置文件路径"""
+        return os.path.join(os.path.dirname(os.path.abspath(__file__)), "amazon_config.json")
+        
+    def save_amazon_config(self):
+        """保存亚马逊配置"""
+        config = {
+            "sku": self.sku_input.get(),
+            "main_image_column": self.main_image_column.get(),
+            "other_image_columns": self.other_image_columns.get(),
+            "template_path": self.amazon_template_path.get()
+        }
+        try:
+            with open(self.get_config_path(), 'w', encoding='utf-8') as f:
+                json.dump(config, f, ensure_ascii=False, indent=2)
+            self.amazon_log("✅ 配置已保存")
+        except Exception as e:
+            self.amazon_log(f"❌ 保存配置失败: {str(e)}")
+            
+    def load_amazon_config(self):
+        """加载亚马逊配置"""
+        try:
+            config_path = self.get_config_path()
+            if os.path.exists(config_path):
+                with open(config_path, 'r', encoding='utf-8') as f:
+                    config = json.load(f)
+                self.sku_input.set(config.get("sku", "bar grey 03"))
+                self.main_image_column.set(config.get("main_image_column", "产品主图"))
+                self.other_image_columns.set(config.get("other_image_columns", "上架图片1,上架图片2,上架图片3,上架图片4,上架图片5,上架图片6,上架图片7,上架图片8"))
+                if config.get("template_path"):
+                    self.amazon_template_path.set(config.get("template_path"))
+                self.amazon_log("✅ 配置已加载")
+            else:
+                self.amazon_log("⚠️ 未找到配置文件,使用默认配置")
+        except Exception as e:
+            self.amazon_log(f"❌ 加载配置失败: {str(e)}")
+            
+    def load_config(self):
+        """启动时自动加载亚马逊配置"""
+        try:
+            config_path = self.get_config_path()
+            if os.path.exists(config_path):
+                with open(config_path, 'r', encoding='utf-8') as f:
+                    config = json.load(f)
+                self.sku_input.set(config.get("sku", "bar grey 03"))
+                self.main_image_column.set(config.get("main_image_column", "产品主图"))
+                self.other_image_columns.set(config.get("other_image_columns", "上架图片1,上架图片2,上架图片3,上架图片4,上架图片5,上架图片6,上架图片7,上架图片8"))
+                if config.get("template_path"):
+                    self.amazon_template_path.set(config.get("template_path"))
+        except Exception:
+            pass
+
     def start_matching(self):
         """开始匹配处理"""
         if not self.template_path.get():
@@ -147,7 +429,6 @@ class ImageMatcherApp:
                 if header == '产品主图':
                     product_main_col = idx
                 elif '上架图片' in header:
-                    # 提取数字
                     match = re.search(r'(\d+)', header)
                     if match:
                         num = int(match.group(1))
@@ -163,7 +444,6 @@ class ImageMatcherApp:
             # 处理每一行(从第三行开始)
             processed_count = 0
             for row_idx in range(3, ws.max_row + 1):
-                # 获取A列的值
                 a_cell = ws.cell(row=row_idx, column=col_a_index)
                 a_value = str(a_cell.value).strip() if a_cell.value else ''
                 
@@ -221,7 +501,6 @@ class ImageMatcherApp:
             
     def find_and_upload_image(self, a_value, num, image_dir):
         """查找图片并上传,返回URL"""
-        # 查找本地图片
         extensions = ['.jpg', '.jpeg', '.png']
         local_file = None
         
@@ -235,11 +514,9 @@ class ImageMatcherApp:
         if not local_file:
             return None
             
-        # 上传图片到服务器
         try:
             full_url = f"{self.upload_url}?group={self.upload_group}&user={self.upload_user}"
             
-            # 根据文件扩展名确定content-type
             content_type = 'image/jpeg'
             if local_file.lower().endswith('.png'):
                 content_type = 'image/png'
@@ -263,6 +540,134 @@ class ImageMatcherApp:
             self.log(f"    上传出错: {str(e)}")
             return None
 
+    def start_amazon_upload(self):
+        """开始亚马逊图片上传"""
+        if not self.amazon_template_path.get():
+            messagebox.showerror("错误", "请先选择模板文件!")
+            return
+        
+        if not self.sku_input.get().strip():
+            messagebox.showerror("错误", "请先输入SKU!")
+            return
+            
+        thread = threading.Thread(target=self.process_amazon_upload, daemon=True)
+        thread.start()
+        
+    def process_amazon_upload(self):
+        """执行亚马逊图片上传"""
+        try:
+            self.amazon_progress_var.set("处理中...")
+            self.amazon_log("=" * 50)
+            self.amazon_log("开始亚马逊图片上传...")
+            
+            template_path = self.amazon_template_path.get()
+            sku = self.sku_input.get().strip()
+            main_img_col_name = self.main_image_column.get()
+            other_cols_str = self.other_image_columns.get()
+            other_col_names = [col.strip() for col in other_cols_str.split(',') if col.strip()]
+            
+            self.amazon_log(f"读取模板文件: {template_path}")
+            self.amazon_log(f"使用的SKU: {sku}")
+            
+            # 读取Excel
+            df = pd.read_excel(template_path)
+            self.amazon_log(f"找到 {len(df)} 行数据")
+            
+            # 检查必要的列
+            if main_img_col_name not in df.columns:
+                self.amazon_log(f"❌ 未找到主图列: {main_img_col_name}")
+                self.amazon_progress_var.set("上传失败")
+                return
+                
+            # 检查附图列
+            existing_other_cols = [col for col in other_col_names if col in df.columns]
+            if not existing_other_cols:
+                self.amazon_log("⚠️ 未找到任何附图列")
+            
+            self.amazon_log(f"主图列: {main_img_col_name}")
+            self.amazon_log(f"附图列: {existing_other_cols}")
+            
+            # 获取Access Token
+            self.amazon_log("🔑 获取Access Token...")
+            try:
+                token = self.amazon_uploader.get_access_token()
+                self.amazon_log("✅ Token获取成功")
+            except Exception as e:
+                self.amazon_log(f"❌ 获取Token失败: {str(e)}")
+                self.amazon_progress_var.set("上传失败")
+                return
+            
+            # 处理每一行
+            success_count = 0
+            fail_count = 0
+            total_rows = len(df)
+            
+            for idx, row in df.iterrows():
+                self.amazon_log(f"\n处理第 {idx+1}/{total_rows} 行")
+                
+                # 处理主图
+                main_img_url = str(row[main_img_col_name]).strip()
+                if main_img_url and main_img_url != 'nan' and main_img_url != 'None':
+                    if main_img_url.startswith('http'):
+                        try:
+                            resp = self.amazon_uploader.patch_image(token, "main", main_img_url, sku)
+                            if resp.status_code == 200:
+                                self.amazon_log(f"  ✅ 主图上传成功")
+                                success_count += 1
+                            else:
+                                self.amazon_log(f"  ❌ 主图上传失败: {resp.status_code}")
+                                fail_count += 1
+                                # 打印错误详情
+                                try:
+                                    error_data = resp.json()
+                                    self.amazon_log(f"  错误详情: {json.dumps(error_data, ensure_ascii=False)[:200]}")
+                                except:
+                                    pass
+                        except Exception as e:
+                            self.amazon_log(f"  ❌ 主图上传异常: {str(e)}")
+                            fail_count += 1
+                    else:
+                        self.amazon_log(f"  ⚠️ 主图URL无效: {main_img_url[:50]}...")
+                else:
+                    self.amazon_log(f"  ⚠️ 主图为空,跳过")
+                
+                # 处理附图
+                for i, col_name in enumerate(existing_other_cols, start=1):
+                    img_url = str(row[col_name]).strip()
+                    if img_url and img_url != 'nan' and img_url != 'None':
+                        if img_url.startswith('http'):
+                            try:
+                                image_type = f"other_{i}"
+                                resp = self.amazon_uploader.patch_image(token, image_type, img_url, sku)
+                                if resp.status_code == 200:
+                                    self.amazon_log(f"  ✅ 附图{i}上传成功")
+                                    success_count += 1
+                                else:
+                                    self.amazon_log(f"  ❌ 附图{i}上传失败: {resp.status_code}")
+                                    fail_count += 1
+                            except Exception as e:
+                                self.amazon_log(f"  ❌ 附图{i}上传异常: {str(e)}")
+                                fail_count += 1
+                        else:
+                            self.amazon_log(f"  ⚠️ 附图{i} URL无效: {img_url[:50]}...")
+                    else:
+                        self.amazon_log(f"  ⚠️ 附图{i}为空,跳过")
+                
+                # 避免请求过快
+                time.sleep(0.5)
+            
+            self.amazon_log(f"\n✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
+            self.amazon_progress_var.set(f"上传完成!成功: {success_count}, 失败: {fail_count}")
+            messagebox.showinfo("完成", f"亚马逊图片上传完成!\n成功: {success_count}\n失败: {fail_count}")
+            
+        except Exception as e:
+            error_msg = f"处理出错: {str(e)}"
+            self.amazon_log(f"❌ {error_msg}")
+            self.amazon_progress_var.set("处理出错")
+            messagebox.showerror("错误", error_msg)
+            import traceback
+            self.amazon_log(traceback.format_exc())
+
 
 def main():
     root = tk.Tk()