PUGE 3 settimane fa
parent
commit
ed331b0cfa
3 ha cambiato i file con 278 aggiunte e 299 eliminazioni
  1. 4 0
      amazon_config.json
  2. 1 0
      控制vm虚拟机.py
  3. 273 299
      自动匹配上架图片.py

+ 4 - 0
amazon_config.json

@@ -0,0 +1,4 @@
+{
+  "sku": "bar table pink",
+  "image_dir": "C:\\Users\\mail\\Documents\\WXWork\\1688853955735391\\Cache\\File\\2026-09\\图片素材\\主图吧台"
+}

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

@@ -1098,6 +1098,7 @@ class VmControlGUI:
     def loop_worker(self):
         # 按键定义:(类型, 键码/标识, 扩展标志, 名称)
         keys = [
+            ("keyboard", 0x35, True, "小键盘5"),      # 小键盘8
             ("keyboard", 0x68, True, "小键盘8"),      # 小键盘8
             ("keyboard", 0x62, True, "小键盘2"),      # 小键盘2
             ("mouse", None, None, "鼠标左键"),         # 鼠标左键(原来的0x25左箭头改成鼠标左键)

+ 273 - 299
自动匹配上架图片.py

@@ -68,10 +68,18 @@ class AmazonImageUploader:
             self.product_type = config["product_type"]
         
     def sha256_hash(self, data):
-        return hashlib.sha256(data.encode("utf-8")).hexdigest()
+        """兼容 str 和 bytes"""
+        if isinstance(data, str):
+            data = data.encode("utf-8")
+        return hashlib.sha256(data).hexdigest()
     
     def hmac_sha256(self, key, msg):
-        return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
+        """返回 bytes 类型的 digest,兼容 str 和 bytes 输入"""
+        if isinstance(key, str):
+            key = key.encode("utf-8")
+        if isinstance(msg, str):
+            msg = msg.encode("utf-8")
+        return hmac.new(key, msg, 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)
@@ -120,7 +128,7 @@ class AmazonImageUploader:
         )
         
         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()
+        signature = self.hmac_sha256(signing_key, string_to_sign).hex()
         
         auth_header = (
             f"{algorithm} Credential={self.aws_access_key}/{credential_scope}, "
@@ -348,45 +356,42 @@ class ImageMatcherApp:
         """创建亚马逊主图更换选项卡"""
         tab = self.tab_amazon
         tab.columnconfigure(1, weight=1)
-        tab.rowconfigure(6, weight=1)
+        tab.rowconfigure(5, weight=1)
         
         # 标题
-        title_label = ttk.Label(tab, text="亚马逊主图/附图批量更换", font=("Arial", 14, "bold"))
+        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)
+        # SKU输入
+        ttk.Label(tab, text="SKU:").grid(row=1, 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)
+        ttk.Entry(tab, textvariable=self.sku_input, width=40).grid(row=1, column=1, sticky=tk.W, pady=5, padx=5)
+        
+        # 图片文件夹选择
+        ttk.Label(tab, text="图片文件夹:").grid(row=2, column=0, sticky=tk.W, pady=5)
+        self.amazon_image_dir = tk.StringVar()
+        ttk.Entry(tab, textvariable=self.amazon_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_amazon_image_dir).grid(row=2, column=2, pady=5, padx=5)
+        
+        # 说明标签
+        note_label = ttk.Label(
+            tab, 
+            text="说明:文件夹中的图片将按文件名排序,第1张作为主图,第2-9张作为附图1-8\n"
+                 "支持格式:.jpg / .jpeg / .png",
+            foreground="gray"
+        )
+        note_label.grid(row=3, column=0, columnspan=3, sticky=tk.W, pady=5, padx=5)
         
         # 按钮框架
         button_frame = ttk.Frame(tab)
-        button_frame.grid(row=5, column=0, columnspan=3, pady=10)
+        button_frame.grid(row=4, 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.grid(row=5, 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)
         
@@ -396,64 +401,70 @@ class ImageMatcherApp:
         # 进度标签
         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)
+        progress_label.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
 
     def create_detail_tab(self):
         """创建亚马逊详情图更换选项卡 (A+ Content)"""
         tab = self.tab_detail
         tab.columnconfigure(1, weight=1)
-        tab.rowconfigure(8, weight=1)
+        tab.rowconfigure(9, weight=1)
         
         # 标题
         title_label = ttk.Label(tab, text="亚马逊详情图更换 (A+ Content)", 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.detail_template_path = tk.StringVar()
-        ttk.Entry(tab, textvariable=self.detail_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_detail_template).grid(row=1, column=2, pady=5, padx=5)
-        
         # ASIN输入
-        ttk.Label(tab, text="ASIN (手动输入):").grid(row=2, column=0, sticky=tk.W, pady=5)
+        ttk.Label(tab, text="ASIN:").grid(row=1, column=0, sticky=tk.W, pady=5)
         self.asin_input = tk.StringVar(value="B0FR1WRK4M")
-        ttk.Entry(tab, textvariable=self.asin_input, width=40).grid(row=2, column=1, sticky=tk.W, pady=5, padx=5)
+        ttk.Entry(tab, textvariable=self.asin_input, width=40).grid(row=1, column=1, sticky=tk.W, pady=5, padx=5)
+        
+        # 标题输入
+        ttk.Label(tab, text="标题:").grid(row=2, column=0, sticky=tk.W, pady=5)
+        self.detail_headline = tk.StringVar(value="Premium Quality Product")
+        ttk.Entry(tab, textvariable=self.detail_headline, width=60).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
         
-        # 图片URL列选择(详情图)
-        ttk.Label(tab, text="详情图列名(逗号分隔):").grid(row=3, column=0, sticky=tk.W, pady=5)
-        self.detail_image_columns = tk.StringVar(value="详情图URL1,详情图URL2,详情图URL3")
-        ttk.Entry(tab, textvariable=self.detail_image_columns, width=60).grid(row=3, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
+        # 描述输入
+        ttk.Label(tab, text="描述:").grid(row=3, column=0, sticky=tk.W, pady=5)
+        self.detail_body = tk.StringVar(value="High-quality product designed for your needs.")
+        ttk.Entry(tab, textvariable=self.detail_body, width=60).grid(row=3, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
+        
+        # 图片文件夹选择
+        ttk.Label(tab, text="图片文件夹:").grid(row=4, column=0, sticky=tk.W, pady=5)
+        self.detail_image_dir = tk.StringVar()
+        ttk.Entry(tab, textvariable=self.detail_image_dir, width=60).grid(row=4, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)
+        ttk.Button(tab, text="选择文件夹", command=self.select_detail_image_dir).grid(row=4, column=2, pady=5, padx=5)
         
         # 图片尺寸设置
-        ttk.Label(tab, text="图片宽度(像素):").grid(row=4, column=0, sticky=tk.W, pady=5)
+        size_frame = ttk.Frame(tab)
+        size_frame.grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=5, padx=5)
+        
+        ttk.Label(size_frame, text="图片宽度(像素):").pack(side=tk.LEFT)
         self.image_width = tk.StringVar(value="970")
-        ttk.Entry(tab, textvariable=self.image_width, width=15).grid(row=4, column=1, sticky=tk.W, pady=5, padx=5)
+        ttk.Entry(size_frame, textvariable=self.image_width, width=10).pack(side=tk.LEFT, padx=5)
         
-        ttk.Label(tab, text="图片高度(像素):").grid(row=5, column=0, sticky=tk.W, pady=5)
+        ttk.Label(size_frame, text="图片高度(像素):").pack(side=tk.LEFT, padx=(20, 0))
         self.image_height = tk.StringVar(value="600")
-        ttk.Entry(tab, textvariable=self.image_height, width=15).grid(row=5, column=1, sticky=tk.W, pady=5, padx=5)
-        
-        # 标题和描述设置
-        ttk.Label(tab, text="模块标题:").grid(row=6, column=0, sticky=tk.W, pady=5)
-        self.module_headline = tk.StringVar(value="Premium Quality Product")
-        ttk.Entry(tab, textvariable=self.module_headline, width=50).grid(row=6, column=1, sticky=tk.W, pady=5, padx=5)
-        
-        ttk.Label(tab, text="模块描述:").grid(row=7, column=0, sticky=tk.W, pady=5)
-        self.module_body = tk.StringVar(value="High-quality product designed for your needs.")
-        ttk.Entry(tab, textvariable=self.module_body, width=50).grid(row=7, column=1, sticky=tk.W, pady=5, padx=5)
+        ttk.Entry(size_frame, textvariable=self.image_height, width=10).pack(side=tk.LEFT, padx=5)
+        
+        # 说明标签
+        note_label = ttk.Label(
+            tab, 
+            text="说明:文件夹中的所有图片将按文件名顺序上传,每张图片作为一个 A+ 模块\n"
+                 "支持格式:.jpg / .jpeg / .png",
+            foreground="gray"
+        )
+        note_label.grid(row=6, column=0, columnspan=3, sticky=tk.W, pady=5, padx=5)
         
         # 按钮框架
         button_frame = ttk.Frame(tab)
-        button_frame.grid(row=8, column=0, columnspan=3, pady=10)
+        button_frame.grid(row=7, column=0, columnspan=3, pady=10)
         
-        ttk.Button(button_frame, text="保存配置", command=self.save_detail_config, width=12).pack(side=tk.LEFT, padx=5)
-        ttk.Button(button_frame, text="加载配置", command=self.load_detail_config, width=12).pack(side=tk.LEFT, padx=5)
         ttk.Button(button_frame, text="开始上传", command=self.start_detail_upload, width=15).pack(side=tk.LEFT, padx=5)
         ttk.Button(button_frame, text="清空日志", command=self.clear_detail_log, width=15).pack(side=tk.LEFT, padx=5)
         
         # 日志框
         log_frame = ttk.LabelFrame(tab, text="上传日志", padding="5")
-        log_frame.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
+        log_frame.grid(row=8, 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)
         
@@ -463,7 +474,7 @@ class ImageMatcherApp:
         # 进度标签
         self.detail_progress_var = tk.StringVar(value="就绪")
         progress_label = ttk.Label(tab, textvariable=self.detail_progress_var, relief=tk.SUNKEN, anchor=tk.W)
-        progress_label.grid(row=10, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
+        progress_label.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
         
     def create_settings_tab(self):
         """创建设置选项卡"""
@@ -712,6 +723,7 @@ class ImageMatcherApp:
         except Exception:
             pass
 
+    # ==================== 文件/目录选择 ====================
     def select_template(self):
         filename = filedialog.askopenfilename(
             title="选择模板文件",
@@ -726,25 +738,20 @@ class ImageMatcherApp:
         if directory:
             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 select_detail_template(self):
-        filename = filedialog.askopenfilename(
-            title="选择模板文件",
-            filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
-        )
-        if filename:
-            self.detail_template_path.set(filename)
-            self.detail_log(f"已选择模板文件: {filename}")
-            
+    def select_amazon_image_dir(self):
+        directory = filedialog.askdirectory(title="选择图片文件夹")
+        if directory:
+            self.amazon_image_dir.set(directory)
+            self.amazon_log(f"已选择图片文件夹: {directory}")
+    
+    def select_detail_image_dir(self):
+        directory = filedialog.askdirectory(title="选择图片文件夹")
+        if directory:
+            self.detail_image_dir.set(directory)
+            self.detail_log(f"已选择图片文件夹: {directory}")
+    
+    # ==================== 日志方法 ====================
     def log(self, message):
         """在匹配选项卡日志框中添加消息"""
         self.log_text.insert(tk.END, f"{message}\n")
@@ -774,100 +781,28 @@ class ImageMatcherApp:
     def clear_detail_log(self):
         """清空详情图选项卡日志"""
         self.detail_log_text.delete(1.0, tk.END)
-        
-    def get_config_path(self):
-        """获取配置文件路径"""
+    
+    # ==================== 配置路径 ====================
+    def get_amazon_config_path(self):
+        """获取主图配置路径"""
         return os.path.join(os.path.dirname(os.path.abspath(__file__)), "amazon_config.json")
     
     def get_detail_config_path(self):
-        """获取详情图配置文件路径"""
+        """获取详情图配置路径"""
         return os.path.join(os.path.dirname(os.path.abspath(__file__)), "amazon_detail_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 save_detail_config(self):
-        """保存详情图配置"""
-        config = {
-            "asin": self.asin_input.get(),
-            "detail_image_columns": self.detail_image_columns.get(),
-            "template_path": self.detail_template_path.get(),
-            "image_width": self.image_width.get(),
-            "image_height": self.image_height.get(),
-            "module_headline": self.module_headline.get(),
-            "module_body": self.module_body.get()
-        }
-        try:
-            with open(self.get_detail_config_path(), 'w', encoding='utf-8') as f:
-                json.dump(config, f, ensure_ascii=False, indent=2)
-            self.detail_log("✅ 配置已保存")
-        except Exception as e:
-            self.detail_log(f"❌ 保存配置失败: {str(e)}")
-            
-    def load_detail_config(self):
-        """加载详情图配置"""
-        try:
-            config_path = self.get_detail_config_path()
-            if os.path.exists(config_path):
-                with open(config_path, 'r', encoding='utf-8') as f:
-                    config = json.load(f)
-                self.asin_input.set(config.get("asin", "B0FR1WRK4M"))
-                self.detail_image_columns.set(config.get("detail_image_columns", "详情图URL1,详情图URL2,详情图URL3"))
-                self.image_width.set(config.get("image_width", "970"))
-                self.image_height.set(config.get("image_height", "600"))
-                self.module_headline.set(config.get("module_headline", "Premium Quality Product"))
-                self.module_body.set(config.get("module_body", "High-quality product designed for your needs."))
-                if config.get("template_path"):
-                    self.detail_template_path.set(config.get("template_path"))
-                self.detail_log("✅ 配置已加载")
-            else:
-                self.detail_log("⚠️ 未找到配置文件,使用默认配置")
-        except Exception as e:
-            self.detail_log(f"❌ 加载配置失败: {str(e)}")
-            
+    # ==================== 配置文件 ====================
     def load_config(self):
-        """启动时自动加载所有配置"""
-        # 加载亚马逊上传配置
+        """启动时自动加载配置"""
+        # 加载主图配置
         try:
-            config_path = self.get_config_path()
+            config_path = self.get_amazon_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"))
+                if config.get("image_dir"):
+                    self.amazon_image_dir.set(config["image_dir"])
         except Exception:
             pass
         
@@ -878,19 +813,19 @@ class ImageMatcherApp:
                 with open(config_path, 'r', encoding='utf-8') as f:
                     config = json.load(f)
                 self.asin_input.set(config.get("asin", "B0FR1WRK4M"))
-                self.detail_image_columns.set(config.get("detail_image_columns", "详情图URL1,详情图URL2,详情图URL3"))
+                self.detail_headline.set(config.get("headline", "Premium Quality Product"))
+                self.detail_body.set(config.get("body", "High-quality product designed for your needs."))
                 self.image_width.set(config.get("image_width", "970"))
                 self.image_height.set(config.get("image_height", "600"))
-                self.module_headline.set(config.get("module_headline", "Premium Quality Product"))
-                self.module_body.set(config.get("module_body", "High-quality product designed for your needs."))
-                if config.get("template_path"):
-                    self.detail_template_path.set(config.get("template_path"))
+                if config.get("image_dir"):
+                    self.detail_image_dir.set(config["image_dir"])
         except Exception:
             pass
         
         # 加载设置配置
         self.load_settings()
 
+    # ==================== 图片匹配上传 ====================
     def start_matching(self):
         """开始匹配处理"""
         if not self.template_path.get():
@@ -1019,6 +954,7 @@ class ImageMatcherApp:
                 break
         
         if not local_file:
+            self.log(f"找不到目录:{filepath}")
             return None
             
         try:
@@ -1047,55 +983,104 @@ 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
+        
+        if not self.amazon_image_dir.get():
+            messagebox.showerror("错误", "请先选择图片文件夹!")
+            return
+        
+        # 保存配置
+        self.save_amazon_config()
             
         thread = threading.Thread(target=self.process_amazon_upload, daemon=True)
         thread.start()
+    
+    def save_amazon_config(self):
+        """保存主图配置"""
+        config = {
+            "sku": self.sku_input.get(),
+            "image_dir": self.amazon_image_dir.get(),
+        }
+        try:
+            with open(self.get_amazon_config_path(), 'w', encoding='utf-8') as f:
+                json.dump(config, f, ensure_ascii=False, indent=2)
+        except Exception:
+            pass
+    
+    def get_image_files(self, folder):
+        """获取文件夹中的图片文件,按文件名排序"""
+        extensions = ['.jpg', '.jpeg', '.png']
+        files = []
+        if not os.path.isdir(folder):
+            return files
+        for f in os.listdir(folder):
+            ext = os.path.splitext(f)[1].lower()
+            if ext in extensions:
+                files.append(os.path.join(folder, f))
+        # 按文件名排序
+        files.sort(key=lambda x: os.path.basename(x).lower())
+        return files
+    
+    def upload_image_to_cos(self, local_file):
+        """上传图片到图片服务器,返回URL"""
+        try:
+            full_url = f"{self.upload_url}?group={self.upload_group}&user={self.upload_user}"
+            
+            content_type = 'image/jpeg'
+            if local_file.lower().endswith('.png'):
+                content_type = 'image/png'
+            
+            with open(local_file, 'rb') as f:
+                files = {'file': (os.path.basename(local_file), f, content_type)}
+                response = requests.post(full_url, files=files, timeout=30)
+                
+                if response.status_code == 200:
+                    result = response.json()
+                    if result.get('err') == 0:
+                        return result.get('url')
+                    else:
+                        raise Exception(result.get('msg', '未知错误'))
+                else:
+                    raise Exception(f"HTTP {response.status_code}")
+        except Exception as e:
+            raise Exception(f"上传失败: {str(e)}")
         
     def process_amazon_upload(self):
-        """执行亚马逊图片上传"""
+        """执行亚马逊主图上传"""
         try:
             self.amazon_progress_var.set("处理中...")
             self.amazon_log("=" * 50)
-            self.amazon_log("开始亚马逊图片上传...")
+            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()]
+            image_dir = self.amazon_image_dir.get()
             
-            self.amazon_log(f"读取模板文件: {template_path}")
-            self.amazon_log(f"使用的SKU: {sku}")
+            self.amazon_log(f"SKU: {sku}")
+            self.amazon_log(f"图片文件夹: {image_dir}")
             
-            # 读取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}")
+            # 获取图片文件
+            image_files = self.get_image_files(image_dir)
+            if not image_files:
+                self.amazon_log("❌ 未找到任何图片文件")
                 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}")
+            self.amazon_log(f"找到 {len(image_files)} 张图片")
+            for i, f in enumerate(image_files, start=1):
+                self.amazon_log(f"  {i}. {os.path.basename(f)}")
+            
+            # 限制最多9张(1主图+8附图)
+            if len(image_files) > 9:
+                self.amazon_log(f"⚠️ 图片数量超过9张,只使用前9张")
+                image_files = image_files[:9]
             
             # 获取Access Token
-            self.amazon_log("🔑 获取Access Token...")
+            self.amazon_log("\n🔑 获取Access Token...")
             try:
                 token = self.amazon_uploader.get_access_token()
                 self.amazon_log("✅ Token获取成功")
@@ -1104,66 +1089,57 @@ class ImageMatcherApp:
                 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} 行")
+            for i, local_file in enumerate(image_files):
+                filename = os.path.basename(local_file)
                 
-                # 处理主图
-                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]}...")
+                # 确定图片类型
+                if i == 0:
+                    image_type = "main"
+                    type_name = "主图"
                 else:
-                    self.amazon_log(f"  ⚠️ 主图为空,跳过")
+                    image_type = f"other_{i}"
+                    type_name = f"附图{i}"
                 
-                # 处理附图
-                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]}...")
+                self.amazon_log(f"\n处理 {type_name}: {filename}")
+                
+                # 上传到图片服务器获取URL
+                try:
+                    self.amazon_log(f"  📤 上传到图片服务器...")
+                    img_url = self.upload_image_to_cos(local_file)
+                    self.amazon_log(f"  ✅ 图片服务器上传成功: {img_url[:60]}...")
+                except Exception as e:
+                    self.amazon_log(f"  ❌ 图片服务器上传失败: {str(e)}")
+                    fail_count += 1
+                    continue
+                
+                # 调用亚马逊API更换图片
+                try:
+                    self.amazon_log(f"  🔄 调用亚马逊API更换{type_name}...")
+                    resp = self.amazon_uploader.patch_image(token, image_type, img_url, sku)
+                    if resp.status_code == 200:
+                        self.amazon_log(f"  ✅ {type_name}更换成功")
+                        success_count += 1
                     else:
-                        self.amazon_log(f"  ⚠️ 附图{i}为空,跳过")
+                        self.amazon_log(f"  ❌ {type_name}更换失败: HTTP {resp.status_code}")
+                        try:
+                            error_data = resp.json()
+                            self.amazon_log(f"  错误详情: {json.dumps(error_data, ensure_ascii=False)[:300]}")
+                        except:
+                            self.amazon_log(f"  响应: {resp.text[:300]}")
+                        fail_count += 1
+                except Exception as e:
+                    self.amazon_log(f"  ❌ {type_name}更换异常: {str(e)}")
+                    fail_count += 1
                 
                 # 避免请求过快
                 time.sleep(0.5)
             
-            self.amazon_log(f"\n✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
+            self.amazon_log(f"\n{'=' * 50}")
+            self.amazon_log(f"✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
             self.amazon_progress_var.set(f"上传完成!成功: {success_count}, 失败: {fail_count}")
             messagebox.showinfo("完成", f"亚马逊图片上传完成!\n成功: {success_count}\n失败: {fail_count}")
             
@@ -1175,19 +1151,39 @@ class ImageMatcherApp:
             import traceback
             self.amazon_log(traceback.format_exc())
 
+    # ==================== 亚马逊详情图更换 ====================
     def start_detail_upload(self):
         """开始亚马逊详情图上传 (A+ Content)"""
-        if not self.detail_template_path.get():
-            messagebox.showerror("错误", "请先选择模板文件!")
-            return
-        
         if not self.asin_input.get().strip():
             messagebox.showerror("错误", "请先输入ASIN!")
             return
+        
+        if not self.detail_image_dir.get():
+            messagebox.showerror("错误", "请先选择图片文件夹!")
+            return
+        
+        # 保存配置
+        self.save_detail_config()
             
         thread = threading.Thread(target=self.process_detail_upload, daemon=True)
         thread.start()
     
+    def save_detail_config(self):
+        """保存详情图配置"""
+        config = {
+            "asin": self.asin_input.get(),
+            "headline": self.detail_headline.get(),
+            "body": self.detail_body.get(),
+            "image_width": self.image_width.get(),
+            "image_height": self.image_height.get(),
+            "image_dir": self.detail_image_dir.get(),
+        }
+        try:
+            with open(self.get_detail_config_path(), 'w', encoding='utf-8') as f:
+                json.dump(config, f, ensure_ascii=False, indent=2)
+        except Exception:
+            pass
+    
     def process_detail_upload(self):
         """执行亚马逊详情图上传 (A+ Content)"""
         try:
@@ -1195,10 +1191,10 @@ class ImageMatcherApp:
             self.detail_log("=" * 50)
             self.detail_log("开始亚马逊详情图上传 (A+ Content)...")
             
-            template_path = self.detail_template_path.get()
             asin = self.asin_input.get().strip()
-            detail_cols_str = self.detail_image_columns.get()
-            detail_col_names = [col.strip() for col in detail_cols_str.split(',') if col.strip()]
+            headline = self.detail_headline.get().strip()
+            body_text = self.detail_body.get().strip()
+            image_dir = self.detail_image_dir.get()
             
             # 获取图片尺寸
             try:
@@ -1209,25 +1205,22 @@ class ImageMatcherApp:
                 self.detail_progress_var.set("上传失败")
                 return
             
-            module_headline = self.module_headline.get().strip()
-            module_body = self.module_body.get().strip()
-            
-            self.detail_log(f"读取模板文件: {template_path}")
-            self.detail_log(f"使用的ASIN: {asin}")
+            self.detail_log(f"ASIN: {asin}")
+            self.detail_log(f"标题: {headline}")
+            self.detail_log(f"描述: {body_text}")
             self.detail_log(f"图片尺寸: {target_width}x{target_height}")
+            self.detail_log(f"图片文件夹: {image_dir}")
             
-            # 读取Excel
-            df = pd.read_excel(template_path)
-            self.detail_log(f"找到 {len(df)} 行数据")
-            
-            # 检查详情图列
-            existing_detail_cols = [col for col in detail_col_names if col in df.columns]
-            if not existing_detail_cols:
-                self.detail_log("❌ 未找到任何详情图列")
+            # 获取图片文件
+            image_files = self.get_image_files(image_dir)
+            if not image_files:
+                self.detail_log("❌ 未找到任何图片文件")
                 self.detail_progress_var.set("上传失败")
                 return
             
-            self.detail_log(f"详情图列: {existing_detail_cols}")
+            self.detail_log(f"\n找到 {len(image_files)} 张图片")
+            for i, f in enumerate(image_files, start=1):
+                self.detail_log(f"  {i}. {os.path.basename(f)}")
             
             # 尝试导入PIL
             try:
@@ -1239,7 +1232,7 @@ class ImageMatcherApp:
                 self.detail_log("⚠️ 未安装Pillow,将使用原始图片尺寸")
             
             # 获取Access Token
-            self.detail_log("🔑 获取Access Token...")
+            self.detail_log("\n🔑 获取Access Token...")
             try:
                 token = self.amazon_uploader.get_access_token()
                 self.detail_log("✅ Token获取成功")
@@ -1248,39 +1241,19 @@ class ImageMatcherApp:
                 self.detail_progress_var.set("上传失败")
                 return
             
-            # 处理每一行
+            # 处理每张图片
             success_count = 0
             fail_count = 0
-            total_rows = min(len(df), 1)  # 每次只处理一行(A+ Content每次只能绑定一个ASIN)
-            
-            # 只处理第一行
-            row = df.iloc[0]
-            
-            # 收集所有图片URL
-            image_urls = []
-            for col_name in existing_detail_cols:
-                img_url = str(row[col_name]).strip()
-                if img_url and img_url != 'nan' and img_url != 'None' and img_url.startswith('http'):
-                    image_urls.append(img_url)
-            
-            if not image_urls:
-                self.detail_log("❌ 未找到有效的图片URL")
-                self.detail_progress_var.set("上传失败")
-                return
-            
-            self.detail_log(f"找到 {len(image_urls)} 张详情图")
-            
-            # 处理每张图片
             upload_ids = []
-            for i, img_url in enumerate(image_urls, start=1):
-                self.detail_log(f"\n处理第 {i} 张图片: {img_url[:60]}...")
+            
+            for i, local_file in enumerate(image_files, start=1):
+                filename = os.path.basename(local_file)
+                self.detail_log(f"\n处理第 {i}/{len(image_files)} 张图片: {filename}")
                 
                 try:
-                    # 下载图片
-                    img_resp = requests.get(img_url, timeout=30)
-                    img_resp.raise_for_status()
-                    
-                    image_binary = img_resp.content
+                    # 读取图片
+                    with open(local_file, 'rb') as f:
+                        image_binary = f.read()
                     
                     # 调整尺寸
                     if HAS_PIL:
@@ -1291,7 +1264,7 @@ class ImageMatcherApp:
                             img_resized = img.resize((target_width, target_height), Image.LANCZOS)
                             buffer = BytesIO()
                             # 判断原图格式
-                            if img_url.lower().endswith('.png'):
+                            if filename.lower().endswith('.png'):
                                 img_resized.save(buffer, format="PNG")
                             else:
                                 img_resized.save(buffer, format="JPEG")
@@ -1306,7 +1279,7 @@ class ImageMatcherApp:
                     
                     # 确定Content-Type
                     content_type = "image/jpeg"
-                    if img_url.lower().endswith('.png'):
+                    if filename.lower().endswith('.png'):
                         content_type = "image/png"
                     
                     self.detail_log(f"  图片大小: {len(image_binary)} bytes")
@@ -1347,7 +1320,7 @@ class ImageMatcherApp:
                 return
             
             # 构建A+ Content内容文档
-            self.detail_log("\n📝 构建A+ Content文档...")
+            self.detail_log(f"\n📝 构建A+ Content文档 ({len(upload_ids)} 个模块)...")
             
             # 构建模块列表 - 每张图片一个模块
             content_module_list = []
@@ -1356,7 +1329,7 @@ class ImageMatcherApp:
                     "contentModuleType": "STANDARD_HEADER_IMAGE_TEXT",
                     "standardHeaderImageText": {
                         "headline": {
-                            "value": module_headline if i == 0 else f"{module_headline} - {i+1}",
+                            "value": headline if i == 0 else f"{headline} - {i+1}",
                             "decoratorSet": []
                         },
                         "block": {
@@ -1381,7 +1354,7 @@ class ImageMatcherApp:
                             "body": {
                                 "textList": [
                                     {
-                                        "value": module_body if i == 0 else f"Product detail view {i+1}",
+                                        "value": body_text if i == 0 else f"Product detail view {i+1}",
                                         "decoratorSet": []
                                     }
                                 ]
@@ -1430,7 +1403,8 @@ class ImageMatcherApp:
                 self.detail_progress_var.set("上传失败")
                 return
             
-            self.detail_log(f"\n✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
+            self.detail_log(f"\n{'=' * 50}")
+            self.detail_log(f"✅ 处理完成!成功: {success_count}, 失败: {fail_count}")
             self.detail_log(f"   Content Key: {content_key}")
             self.detail_log(f"   ASIN: {asin}")
             self.detail_progress_var.set(f"上传完成!成功: {success_count}, 失败: {fail_count}")