飞书提交数据.py 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157
  1. import tkinter as tk
  2. from tkinter import ttk, filedialog, messagebox, scrolledtext
  3. import pandas as pd
  4. import requests
  5. import json
  6. import threading
  7. import time
  8. import random
  9. import re
  10. from datetime import datetime
  11. import os
  12. import shutil
  13. from openpyxl import load_workbook
  14. from openpyxl.utils import get_column_letter
  15. from PIL import Image
  16. import io
  17. import base64
  18. import zlib
  19. class FeishuSubmitApp:
  20. def __init__(self, root):
  21. self.root = root
  22. self.root.title("飞书表格数据提交工具 - 自动提取图片")
  23. self.root.geometry("1200x900")
  24. # ========== 状态变量 ==========
  25. self.is_running = False
  26. self.stop_flag = False
  27. self.excel_file = ""
  28. self.cookie_string = ""
  29. # 新增:暂停与阈值状态
  30. self.is_paused = False
  31. self.sent_count = 0 # 本次运行已成功发送计数
  32. self.pause_threshold = 0 # 0表示不限制
  33. self.schedule_running = False
  34. self.schedule_thread = None
  35. # 图片缓存
  36. self.image_cache = {} # {行号: [图片数据列表]}
  37. self.image_extract_folder = None
  38. self.uploaded_cache = {} # 缓存已上传的图片token {行号: attachment_info}
  39. # 从curl提取的默认配置
  40. self.default_headers = {
  41. "accept": "application/json, text/plain, */*",
  42. "accept-language": "zh-CN,zh;q=0.9",
  43. "content-type": "application/json",
  44. "f-version": "docs-6-17-1781628518399",
  45. "origin": "https://xhd.feishu.cn",
  46. "priority": "u=1, i",
  47. "referer": "https://xhd.feishu.cn/share/base/form/shrcnJqFUFF0sqlmrfsWpDNf8df",
  48. "sec-ch-ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
  49. "sec-ch-ua-mobile": "?0",
  50. "sec-ch-ua-platform": '"Windows"',
  51. "sec-fetch-dest": "empty",
  52. "sec-fetch-mode": "cors",
  53. "sec-fetch-site": "same-origin",
  54. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
  55. "x-csrftoken": "83aae12269bced10680d3ccf817f82262305e7d0-1781579108",
  56. "x-tt-logid": "02178275091573600000000000000000000ffff5bd640caae0cb8",
  57. "x-tt-trace": "1"
  58. }
  59. self.url = "https://xhd.feishu.cn/space/api/bitable/share/content"
  60. self.share_token = "shrcnJqFUFF0sqlmrfsWpDNf8df"
  61. self.setup_ui()
  62. self.load_config()
  63. self.start_schedule() # 启动定时调度器
  64. def setup_ui(self):
  65. """构建界面"""
  66. # 主容器
  67. main_frame = ttk.Frame(self.root, padding="10")
  68. main_frame.pack(fill=tk.BOTH, expand=True)
  69. # ========== 第一行:文件选择 ==========
  70. file_frame = ttk.LabelFrame(main_frame, text="📁 数据文件", padding="10")
  71. file_frame.pack(fill=tk.X, pady=(0, 10))
  72. ttk.Label(file_frame, text="Excel文件:").grid(row=0, column=0, sticky=tk.W)
  73. self.file_path_var = tk.StringVar()
  74. ttk.Entry(file_frame, textvariable=self.file_path_var, width=60).grid(row=0, column=1, padx=(10, 10))
  75. ttk.Button(file_frame, text="浏览...", command=self.select_file).grid(row=0, column=2)
  76. ttk.Label(file_frame, text="工作表:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
  77. self.sheet_var = tk.StringVar(value="Sheet1")
  78. ttk.Entry(file_frame, textvariable=self.sheet_var, width=20).grid(row=1, column=1, sticky=tk.W, padx=(10, 10), pady=(10, 0))
  79. ttk.Label(file_frame, text="(默认: Sheet1)").grid(row=1, column=2, sticky=tk.W, pady=(10, 0))
  80. # ========== 第二行:Cookie配置 ==========
  81. cookie_frame = ttk.LabelFrame(main_frame, text="🍪 Cookie 配置", padding="10")
  82. cookie_frame.pack(fill=tk.X, pady=(0, 10))
  83. ttk.Label(cookie_frame, text="Cookie:").grid(row=0, column=0, sticky=tk.W)
  84. self.cookie_text = tk.Text(cookie_frame, height=4, width=80, wrap=tk.WORD)
  85. self.cookie_text.grid(row=0, column=1, padx=(10, 10), pady=(0, 5), sticky=tk.W)
  86. cookie_btn_frame = ttk.Frame(cookie_frame)
  87. cookie_btn_frame.grid(row=1, column=1, sticky=tk.W, padx=(10, 0))
  88. ttk.Button(cookie_btn_frame, text="从剪贴板粘贴", command=self.paste_cookie).pack(side=tk.LEFT, padx=(0, 10))
  89. ttk.Button(cookie_btn_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=(0, 10))
  90. ttk.Button(cookie_btn_frame, text="加载默认", command=self.load_default_cookie).pack(side=tk.LEFT)
  91. # ========== 第三行:发送控制 + 定时 + 阈值 ==========
  92. control_frame = ttk.LabelFrame(main_frame, text="⚙️ 发送控制 / ⏰ 定时调度 / 🚦 阈值限制", padding="10")
  93. control_frame.pack(fill=tk.X, pady=(0, 10))
  94. # --- 3.1 基础间隔配置 ---
  95. ttk.Label(control_frame, text="最小间隔(秒):").grid(row=0, column=0, sticky=tk.W)
  96. self.min_interval_var = tk.StringVar(value="20")
  97. ttk.Entry(control_frame, textvariable=self.min_interval_var, width=8).grid(row=0, column=1, padx=(5, 15), sticky=tk.W)
  98. ttk.Label(control_frame, text="最大间隔(秒):").grid(row=0, column=2, sticky=tk.W)
  99. self.max_interval_var = tk.StringVar(value="50")
  100. ttk.Entry(control_frame, textvariable=self.max_interval_var, width=8).grid(row=0, column=3, padx=(5, 15), sticky=tk.W)
  101. ttk.Label(control_frame, text="起始行:").grid(row=0, column=4, sticky=tk.W)
  102. self.start_row_var = tk.StringVar(value="2")
  103. ttk.Entry(control_frame, textvariable=self.start_row_var, width=8).grid(row=0, column=5, padx=(5, 15), sticky=tk.W)
  104. ttk.Label(control_frame, text="(表头在第1行)").grid(row=0, column=6, sticky=tk.W)
  105. # --- 3.2 定时自动启停 ---
  106. ttk.Separator(control_frame, orient=tk.HORIZONTAL).grid(row=1, column=0, columnspan=7, sticky=tk.EW, pady=(12, 8))
  107. ttk.Label(control_frame, text="⏰ 自动启动时间:").grid(row=2, column=0, sticky=tk.W)
  108. self.start_time_var = tk.StringVar(value="")
  109. ttk.Entry(control_frame, textvariable=self.start_time_var, width=10).grid(row=2, column=1, padx=(5, 5), sticky=tk.W)
  110. ttk.Label(control_frame, text="(HH:MM, 留空则不自动启动)").grid(row=2, column=2, sticky=tk.W, columnspan=2)
  111. ttk.Label(control_frame, text="自动关闭时间:").grid(row=2, column=4, sticky=tk.W)
  112. self.stop_time_var = tk.StringVar(value="")
  113. ttk.Entry(control_frame, textvariable=self.stop_time_var, width=10).grid(row=2, column=5, padx=(5, 5), sticky=tk.W)
  114. ttk.Label(control_frame, text="(HH:MM, 留空则不自动关闭)").grid(row=2, column=6, sticky=tk.W)
  115. # --- 3.3 阈值限制 ---
  116. ttk.Separator(control_frame, orient=tk.HORIZONTAL).grid(row=3, column=0, columnspan=7, sticky=tk.EW, pady=(12, 8))
  117. ttk.Label(control_frame, text="🚦 录入阈值(条):").grid(row=4, column=0, sticky=tk.W)
  118. self.threshold_var = tk.StringVar(value="0")
  119. ttk.Entry(control_frame, textvariable=self.threshold_var, width=10).grid(row=4, column=1, padx=(5, 5), sticky=tk.W)
  120. ttk.Label(control_frame, text="(0=不限制, 达到后自动暂停)").grid(row=4, column=2, sticky=tk.W, columnspan=2)
  121. # 已发送计数显示
  122. self.count_display_var = tk.StringVar(value="本次已发送: 0 条")
  123. ttk.Label(control_frame, textvariable=self.count_display_var, foreground="green", font=("Microsoft YaHei", 9, "bold")).grid(row=4, column=4, columnspan=3, sticky=tk.W)
  124. # --- 3.4 操作按钮 ---
  125. btn_frame = ttk.Frame(control_frame)
  126. btn_frame.grid(row=5, column=0, columnspan=7, pady=(15, 0), sticky=tk.W)
  127. self.start_btn = ttk.Button(btn_frame, text="🚀 开始发送", command=self.start_send)
  128. self.start_btn.pack(side=tk.LEFT, padx=(0, 10))
  129. self.stop_btn = ttk.Button(btn_frame, text="⏹ 停止", command=self.stop_send, state=tk.DISABLED)
  130. self.stop_btn.pack(side=tk.LEFT, padx=(0, 10))
  131. # 新增:暂停/继续按钮
  132. self.pause_btn = ttk.Button(btn_frame, text="⏸ 暂停", command=self.toggle_pause, state=tk.DISABLED)
  133. self.pause_btn.pack(side=tk.LEFT, padx=(0, 10))
  134. ttk.Button(btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT, padx=(0, 10))
  135. ttk.Button(btn_frame, text="📸 提取图片", command=self.extract_images).pack(side=tk.LEFT)
  136. # 状态显示
  137. self.status_var = tk.StringVar(value="就绪 | 定时调度: 运行中")
  138. ttk.Label(control_frame, textvariable=self.status_var, foreground="blue").grid(row=6, column=0, columnspan=7, pady=(10, 0), sticky=tk.W)
  139. # ========== 日志显示 ==========
  140. log_frame = ttk.LabelFrame(main_frame, text="📋 发送日志", padding="10")
  141. log_frame.pack(fill=tk.BOTH, expand=True)
  142. self.log_text = scrolledtext.ScrolledText(log_frame, height=20, wrap=tk.WORD, font=("Consolas", 9))
  143. self.log_text.pack(fill=tk.BOTH, expand=True)
  144. # 日志颜色标签
  145. self.log_text.tag_config("success", foreground="green")
  146. self.log_text.tag_config("error", foreground="red")
  147. self.log_text.tag_config("info", foreground="blue")
  148. self.log_text.tag_config("warning", foreground="orange")
  149. self.log_text.tag_config("debug", foreground="purple")
  150. self.log_text.tag_config("pause", foreground="#FF6600")
  151. self.log_text.tag_config("schedule", foreground="#0066CC")
  152. # ========== 底部状态栏 ==========
  153. status_bar = ttk.Frame(self.root)
  154. status_bar.pack(fill=tk.X, side=tk.BOTTOM)
  155. ttk.Label(status_bar, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W).pack(fill=tk.X, padx=5, pady=2)
  156. # ==================== 定时调度相关 ====================
  157. def start_schedule(self):
  158. """启动后台定时调度线程"""
  159. self.schedule_running = True
  160. self.schedule_thread = threading.Thread(target=self._schedule_worker, daemon=True)
  161. self.schedule_thread.start()
  162. self.log("⏰ 定时调度器已启动,将按设定时间自动启停", "schedule")
  163. def stop_schedule(self):
  164. """停止定时调度线程"""
  165. self.schedule_running = False
  166. if self.schedule_thread and self.schedule_thread.is_alive():
  167. # 由于线程在sleep,无法直接中断,设置标志即可
  168. pass
  169. def _schedule_worker(self):
  170. """后台线程:每秒检查一次时间,触发自动启停"""
  171. while self.schedule_running:
  172. try:
  173. now = datetime.now()
  174. current_time_str = now.strftime("%H:%M")
  175. start_time = self.start_time_var.get().strip()
  176. stop_time = self.stop_time_var.get().strip()
  177. # 自动启动检查
  178. if start_time and not self.is_running and not self.is_paused:
  179. if current_time_str == start_time and now.second < 2: # 在目标时间的0-1秒内触发
  180. self.log(f"⏰ 到达自动启动时间 {start_time},即将开始发送...", "schedule")
  181. # 在主线程中调用启动(通过after确保线程安全)
  182. self.root.after(0, self._safe_auto_start)
  183. # 自动关闭检查
  184. if stop_time and self.is_running:
  185. if current_time_str == stop_time and now.second < 2:
  186. self.log(f"⏰ 到达自动关闭时间 {stop_time},即将停止发送...", "schedule")
  187. self.root.after(0, self._safe_auto_stop)
  188. time.sleep(1)
  189. except Exception as e:
  190. time.sleep(1)
  191. def _safe_auto_start(self):
  192. """线程安全的自动启动(在主线程执行)"""
  193. if not self.is_running:
  194. self.start_send()
  195. def _safe_auto_stop(self):
  196. """线程安全的自动停止(在主线程执行)"""
  197. if self.is_running:
  198. self.stop_send()
  199. # ==================== 暂停/继续 ====================
  200. def toggle_pause(self):
  201. """暂停/继续切换"""
  202. if not self.is_running:
  203. return
  204. if self.is_paused:
  205. # 继续
  206. self.is_paused = False
  207. self.pause_btn.config(text="⏸ 暂停")
  208. self.log("▶️ 已继续发送", "pause")
  209. self.status_var.set("🔄 发送中... (已继续)")
  210. else:
  211. # 暂停
  212. self.is_paused = True
  213. self.pause_btn.config(text="▶️ 继续")
  214. self.log("⏸ 已暂停发送(当前请求完成后暂停)", "pause")
  215. self.status_var.set("⏸ 已暂停")
  216. def _check_threshold(self):
  217. """检查是否达到录入阈值"""
  218. threshold_str = self.threshold_var.get().strip()
  219. if not threshold_str or threshold_str == "0":
  220. return False
  221. try:
  222. threshold = int(threshold_str)
  223. if threshold <= 0:
  224. return False
  225. if self.sent_count >= threshold:
  226. self.is_paused = True
  227. self.pause_btn.config(text="▶️ 继续")
  228. self.log(f"🚦 已达到录入阈值 {threshold} 条,自动暂停!", "pause")
  229. self.status_var.set(f"⏸ 已达阈值 {threshold} 条,已自动暂停")
  230. return True
  231. except ValueError:
  232. pass
  233. return False
  234. def reset_count(self):
  235. """重置已发送计数"""
  236. self.sent_count = 0
  237. self.count_display_var.set("本次已发送: 0 条")
  238. self.log("🔄 已重置发送计数", "info")
  239. # ==================== 原有方法(修改部分标注) ====================
  240. def select_file(self):
  241. """选择Excel文件"""
  242. file_path = filedialog.askopenfilename(
  243. title="选择Excel文件",
  244. filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
  245. )
  246. if file_path:
  247. self.file_path_var.set(file_path)
  248. self.excel_file = file_path
  249. self.log(f"已选择文件: {file_path}", "info")
  250. try:
  251. xl = pd.ExcelFile(file_path)
  252. sheets = xl.sheet_names
  253. if sheets:
  254. self.sheet_var.set(sheets[0])
  255. self.log(f"自动选择工作表: {sheets[0]}", "info")
  256. except Exception as e:
  257. self.log(f"读取工作表失败: {str(e)}", "warning")
  258. def paste_cookie(self):
  259. """从剪贴板粘贴Cookie"""
  260. try:
  261. clipboard_text = self.root.clipboard_get()
  262. if "cookie:" in clipboard_text.lower() or "-H" in clipboard_text:
  263. cookie_match = re.search(r'-b\s+"([^"]+)"', clipboard_text)
  264. if not cookie_match:
  265. cookie_match = re.search(r'-H\s+"cookie:\s*([^"]+)"', clipboard_text, re.IGNORECASE)
  266. if cookie_match:
  267. self.cookie_text.delete(1.0, tk.END)
  268. self.cookie_text.insert(1.0, cookie_match.group(1))
  269. self.log("已从剪贴板提取Cookie", "success")
  270. return
  271. self.cookie_text.delete(1.0, tk.END)
  272. self.cookie_text.insert(1.0, clipboard_text)
  273. self.log("已粘贴Cookie", "info")
  274. except:
  275. messagebox.showwarning("提示", "无法读取剪贴板内容")
  276. def load_default_cookie(self):
  277. """加载默认Cookie"""
  278. default_cookie = "_uuid_hera_ab_path_1=7651649498397052109; __tea__ug__uid=4144561781538482924; _gcl_au=1.1.775810441.1781538483; lgw_csrf_token=9dcbe9c422a1a2df5727bebebf4136760e1ad6dc-1781538485; passport_web_did=7651823325329411260; passport_trace_id=7651823325353757894; QXV0aHpDb250ZXh0=ba539bcfe4a042c8bc1e99d4d03add38; s_v_web_id=verify_mqg251ox_Tp4ofGaR_NCf4_4zII_8Ad3_c3HxGb4PXIuN; session=XN0YXJ0-5b9t9966-1122-4520-890c-e9313b1f818a-WVuZA; session_list=XN0YXJ0-5b9t9966-1122-4520-890c-e9313b1f818a-WVuZA; login_recently=1; is_anonymous_session=; _csrf_token=83aae12269bced10680d3ccf817f82262305e7d0-1781579108; lang=zh; et=c0c63f4480fdf14d7e6e9a21bab2b7e1; ccm_cdn_host=//lf-package-cn.feishucdn.com/obj/feishu-static; locale=zh-CN; landing_url=https://www.feishu.cn/; Hm_lvt_a79616d9322d81f12a92402ac6ae32ea=1781538483,1781579751; Hm_lpvt_a79616d9322d81f12a92402ac6ae32ea=1781579751; HMACCOUNT=5CA5EB76287246C7; _ga_VPYRHN104D=GS2.1.s1781579004$o2$g1$t1781579751$j60$l0$h0; _uetvid=9877555068d111f1992d03b61d301033; _ga=GA1.2.692261332.1781538484; i18n_locale=zh-CN; i18n_locale=zh-CN; _gid=GA1.2.1918132955.1781749497; site_env=pre=0; sl_session=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODE3OTMyNTgsInVuaXQiOiJldV9uYyIsInJhdyI6eyJtZXRhIjoiQVdvYzIrdU1BNHZSYUhkQjl4N2lRQUpxTUx6RjlrN012R293dk1YMlRzeThhakM5WXdRQURMSUNLZ0VBUVVGQlFVRkJRVUZCUVVKeFRVd3hha2h6UkUxNFVUMDkiLCJzdW0iOiI1MWU3ZWM3YzU0NTAzNTljMzRhMjE3ODdlM2U1OGNmZjQzNzZmM2IzYjJjNzU4NWVjMGE3MjQyZDZjNTBiNmNkIiwibG9jIjoiemhfY24iLCJhcGMiOiJSZWxlYXNlIiwiaWF0IjoxNzgxNzUwMDU4LCJzYWMiOnsiVXNlclN0YWZmU3RhdHVzIjoiMSIsIlVzZXJUeXBlIjoiNDIifSwibG9kIjpudWxsLCJjbmYiOnsiamt0Ijoic3VWaTdnNDNJX0lUb0w2ZGo5QXhEWnBacUhkYldzNWJaaVhvV0s4akZrdyJ9LCJucyI6ImxhcmsiLCJuc191aWQiOiI3NjQ2MjI4MDcyMDgxMDk5NzI5IiwibnNfdGlkIjoiNzUyNzU1NzgzMTgwMzk0NDk2MiIsIm90IjoxLCJjdCI6MTc4MTU3OTEwNywicnQiOjE3ODE2Njg4OTMsInRjcyI6MiwidGJ0IjowfX0.6MrIkLX5grSs5PUBr3U-TC5keonxZZDADzbJ0c00y9HsalJhhQ0gbmfk2u2w5RWhMkXt46a50yMc9E7Po6kymg; msToken=Oyg-XUAwfnJlSSSvRBdj1W_LxhRCXLlWMx9mk0Ryxvku6FZLH0ysyeYnmGrIG_yvUClyZT2cLYe5rhC1PtVygRRXOhYTvrDcL8Oidp-ZHHI-LNFKPJk-She-3kUxNTnwb3fYj5FfI36vX-1eFZ_neFVmoTtgD1GhK3M3oNjh5H-Y; swp_csrf_token=6fe8dbf3-4ab2-4f46-b4f3-5c9cba7364cd; t_beda37=a427edf4f48d9fa049a454a58600fa2f188ffacaf053a85e3782d6615e737b03; passport_app_access_token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODE3OTU2NDcsInVuaXQiOiJldV9uYyIsInJhdyI6eyJtX2FjY2Vzc19pbmZvIjp7IjE0MyI6eyJpYXQiOjE3ODE3NDk1MTksImFjY2VzcyI6dHJ1ZX0sIjIiOnsiaWF0IjoxNzgxNzUyMTY1LCJhY2Nlc3MiOnRydWV9LCIxMDAiOnsiaWF0IjoxNzgxNzUyMjk1LCJhY2Nlc3MiOnRydWV9LCI0Ijp7ImlhdCI6MTc4MTc1MjQ0NywiYWNjZXNzIjp0cnVlfX0sInN1bSI6IjUxZTdlYzdjNTQ1MDM1OWMzNGEyMTc4N2UzZTU4Y2ZmNDM3NmYzYjNiMmM3NTg1ZWMwYTcyNDJkNmM1MGI2Y2QifX0.6o43emSe8iOBsXTPUbx4cShf-DdIpGuTTYDR5NDAAvA6ZByV_biiboF32sk5SjLAuTfEEy1xWpZ_35Fb-nxvJg"
  279. self.cookie_text.delete(1.0, tk.END)
  280. self.cookie_text.insert(1.0, default_cookie)
  281. self.log("已加载默认Cookie", "info")
  282. def save_config(self):
  283. """保存Cookie配置到文件"""
  284. cookie = self.cookie_text.get(1.0, tk.END).strip()
  285. if not cookie:
  286. messagebox.showwarning("提示", "Cookie不能为空")
  287. return
  288. try:
  289. with open("cookie_config.txt", "w", encoding="utf-8") as f:
  290. f.write(cookie)
  291. self.log("Cookie配置已保存", "success")
  292. messagebox.showinfo("成功", "Cookie配置已保存到 cookie_config.txt")
  293. except Exception as e:
  294. self.log(f"保存失败: {str(e)}", "error")
  295. def load_config(self):
  296. """加载保存的Cookie配置"""
  297. try:
  298. with open("cookie_config.txt", "r", encoding="utf-8") as f:
  299. cookie = f.read().strip()
  300. if cookie:
  301. self.cookie_text.delete(1.0, tk.END)
  302. self.cookie_text.insert(1.0, cookie)
  303. self.log("已加载保存的Cookie配置", "info")
  304. except FileNotFoundError:
  305. pass
  306. except Exception as e:
  307. self.log(f"加载配置失败: {str(e)}", "warning")
  308. def log(self, message, tag="info"):
  309. """添加日志"""
  310. timestamp = datetime.now().strftime("%H:%M:%S")
  311. self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", tag)
  312. self.log_text.see(tk.END)
  313. self.root.update_idletasks()
  314. def clear_log(self):
  315. """清空日志"""
  316. self.log_text.delete(1.0, tk.END)
  317. def parse_cookie(self, cookie_string):
  318. """解析Cookie字符串为字典"""
  319. cookies = {}
  320. for item in cookie_string.split(';'):
  321. item = item.strip()
  322. if '=' in item:
  323. key, value = item.split('=', 1)
  324. cookies[key.strip()] = value.strip()
  325. return cookies
  326. def _get_csrf_token(self):
  327. """从cookie中提取csrf_token"""
  328. cookie_text = self.cookie_text.get(1.0, tk.END).strip()
  329. match = re.search(r'_csrf_token=([^;]+)', cookie_text)
  330. if match:
  331. return match.group(1)
  332. match = re.search(r'x-csrftoken[=:]\s*([^;\s]+)', cookie_text, re.IGNORECASE)
  333. if match:
  334. return match.group(1)
  335. return "83aae12269bced10680d3ccf817f82262305e7d0-1781579108"
  336. def _get_mime_type(self, filename):
  337. """获取文件MIME类型"""
  338. ext = os.path.splitext(filename)[1].lower()
  339. mime_map = {
  340. '.jpg': 'image/jpeg',
  341. '.jpeg': 'image/jpeg',
  342. '.png': 'image/png',
  343. '.gif': 'image/gif',
  344. '.bmp': 'image/bmp',
  345. '.webp': 'image/webp'
  346. }
  347. return mime_map.get(ext, 'image/jpeg')
  348. def upload_image(self, image_data, filename="image.jpg"):
  349. """完整上传图片到飞书,返回file_token信息"""
  350. try:
  351. cookie_string = self.cookie_text.get(1.0, tk.END).strip()
  352. if not cookie_string:
  353. self.log(" ❌ Cookie为空,无法上传图片", "error")
  354. return None
  355. cookies = self.parse_cookie(cookie_string)
  356. csrf_token = self._get_csrf_token()
  357. self.log(f" 🔍 使用的CSRF token: {csrf_token[:30] if csrf_token else 'None'}...", "debug")
  358. self.log(f" 🔍 图片大小: {len(image_data)} bytes", "debug")
  359. base_headers = {
  360. "accept": "application/json, text/plain, */*",
  361. "accept-language": "zh-CN,zh;q=0.9",
  362. "f-version": "docs-6-17-1781628518399",
  363. "x-csrftoken": csrf_token,
  364. "x-tt-logid": "02178275091573600000000000000000000ffff5bd640caae0cb8",
  365. "x-tt-trace": "1",
  366. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  367. }
  368. # 步骤1: 获取uploadCode
  369. self.log(f" 📤 [1/4] 获取上传code...", "info")
  370. code_url = "https://xhd.feishu.cn/space/api/bitable/external/share/uploadCode"
  371. code_params = {
  372. "shareToken": self.share_token,
  373. "fileName": filename,
  374. "size": len(image_data),
  375. "mountPoint": "bitable_tmp_point"
  376. }
  377. code_response = requests.get(code_url, params=code_params, headers=base_headers, cookies=cookies, timeout=30)
  378. if code_response.status_code != 200:
  379. self.log(f" ❌ 获取code失败: HTTP {code_response.status_code}", "error")
  380. return None
  381. try:
  382. code_data = code_response.json()
  383. except Exception as e:
  384. self.log(f" ❌ 解析JSON失败: {str(e)}", "error")
  385. return None
  386. upload_code = code_data.get('data', {}).get('uploadCode')
  387. if not upload_code:
  388. self.log(f" ❌ 响应中没有uploadCode字段", "error")
  389. return None
  390. self.log(f" ✅ 获取uploadCode成功", "success")
  391. # 步骤2: 准备上传
  392. self.log(f" 📤 [2/4] 准备上传,获取upload_id...", "info")
  393. prepare_url = "https://xhd.feishu.cn/space/api/box/upload/prepare/authcode/"
  394. prepare_headers = base_headers.copy()
  395. prepare_headers["content-type"] = "application/json"
  396. prepare_headers["x-command"] = "space.api.box.upload.prepare.authcode"
  397. prepare_body = {
  398. "code": upload_code,
  399. "mount_point": "bitable_tmp_point",
  400. "mount_node_token": self.share_token
  401. }
  402. prepare_response = requests.post(prepare_url, headers=prepare_headers, cookies=cookies, json=prepare_body, timeout=30)
  403. if prepare_response.status_code != 200:
  404. self.log(f" ❌ 准备上传失败: HTTP {prepare_response.status_code}", "error")
  405. return None
  406. try:
  407. prepare_data = prepare_response.json()
  408. except Exception as e:
  409. self.log(f" ❌ 解析JSON失败: {str(e)}", "error")
  410. return None
  411. if prepare_data.get('code') != 0:
  412. self.log(f" ❌ 准备上传失败: code={prepare_data.get('code')}", "error")
  413. return None
  414. upload_id = prepare_data.get('data', {}).get('upload_id')
  415. if not upload_id:
  416. self.log(f" ❌ 响应中没有upload_id字段", "error")
  417. return None
  418. self.log(f" ✅ 获取upload_id成功: {upload_id}", "success")
  419. # 步骤3: 上传文件内容
  420. self.log(f" 📤 [3/4] 上传文件内容 ({len(image_data)} bytes)...", "info")
  421. checksum = zlib.adler32(image_data) & 0xffffffff
  422. upload_url = f"https://internal-api-drive-stream.feishu.cn/space/api/box/stream/upload/merge_block/?upload_id={upload_id}"
  423. upload_headers = base_headers.copy()
  424. upload_headers["content-type"] = "application/octet-stream"
  425. upload_headers["x-command"] = "space.api.box.stream.upload.merge_block"
  426. upload_headers["x-seq-list"] = "0"
  427. upload_headers["x-block-list-checksum"] = str(checksum)
  428. upload_headers["x-block-origin-size"] = str(len(image_data))
  429. upload_response = requests.post(upload_url, headers=upload_headers, cookies=cookies, data=image_data, timeout=60)
  430. if upload_response.status_code != 200:
  431. self.log(f" ❌ 上传文件失败: HTTP {upload_response.status_code}", "error")
  432. return None
  433. try:
  434. upload_result = upload_response.json()
  435. if upload_result.get('code') != 0:
  436. self.log(f" ❌ 上传文件失败: code={upload_result.get('code')}", "error")
  437. return None
  438. except:
  439. pass
  440. self.log(f" ✅ 文件上传成功", "success")
  441. # 步骤4: 完成上传
  442. self.log(f" 📤 [4/4] 完成上传,获取file_token...", "info")
  443. finish_url = "https://xhd.feishu.cn/space/api/box/upload/finish/"
  444. finish_headers = base_headers.copy()
  445. finish_headers["content-type"] = "application/json"
  446. finish_headers["x-command"] = "space.api.box.upload.finish"
  447. finish_body = {
  448. "upload_id": upload_id,
  449. "num_blocks": 1,
  450. "push_open_history_record": 0
  451. }
  452. finish_response = requests.post(finish_url, headers=finish_headers, cookies=cookies, json=finish_body, timeout=30)
  453. if finish_response.status_code != 200:
  454. self.log(f" ❌ 完成上传失败: HTTP {finish_response.status_code}", "error")
  455. return None
  456. try:
  457. finish_data = finish_response.json()
  458. except Exception as e:
  459. self.log(f" ❌ 解析JSON失败: {str(e)}", "error")
  460. return None
  461. if finish_data.get('code') != 0:
  462. self.log(f" ❌ 完成上传失败: code={finish_data.get('code')}", "error")
  463. return None
  464. file_token = finish_data.get('data', {}).get('file_token')
  465. if not file_token:
  466. self.log(f" ❌ 响应中没有file_token字段", "error")
  467. return None
  468. width, height = 0, 0
  469. try:
  470. pil_img = Image.open(io.BytesIO(image_data))
  471. width, height = pil_img.size
  472. except:
  473. pass
  474. result = {
  475. 'attachmentToken': file_token,
  476. 'id': file_token,
  477. 'mimeType': self._get_mime_type(filename),
  478. 'name': filename,
  479. 'size': len(image_data),
  480. 'timeStamp': int(time.time() * 1000),
  481. 'height': height,
  482. 'width': width
  483. }
  484. self.log(f" ✅ 图片上传完成! file_token: {file_token}", "success")
  485. return result
  486. except requests.exceptions.Timeout:
  487. self.log(f" ❌ 上传超时", "error")
  488. return None
  489. except Exception as e:
  490. self.log(f" ❌ 上传图片异常: {str(e)}", "error")
  491. import traceback
  492. self.log(f" 详细: {traceback.format_exc()}", "debug")
  493. return None
  494. def extract_images_to_cache(self, file_path, sheet_name):
  495. """提取Excel中的所有图片到缓存"""
  496. image_cache = {}
  497. try:
  498. wb = load_workbook(file_path, data_only=True)
  499. ws = wb[sheet_name]
  500. if hasattr(ws, '_images') and ws._images:
  501. self.log(f" 📸 开始提取 {len(ws._images)} 张图片到缓存...", "info")
  502. for idx, img in enumerate(ws._images):
  503. try:
  504. if hasattr(img, 'anchor') and hasattr(img.anchor, '_from'):
  505. anchor_from = img.anchor._from
  506. row = anchor_from.row + 1
  507. col = anchor_from.col + 1
  508. else:
  509. row = idx + 1
  510. col = 0
  511. image_data = None
  512. if hasattr(img, '_data') and callable(img._data):
  513. image_data = img._data()
  514. elif hasattr(img, '_data') and not callable(img._data):
  515. image_data = img._data
  516. elif hasattr(img, 'image'):
  517. if hasattr(img.image, '_data') and callable(img.image._data):
  518. image_data = img.image._data()
  519. elif hasattr(img.image, '_data'):
  520. image_data = img.image._data
  521. if image_data is None:
  522. continue
  523. img_format = 'jpg'
  524. filename = f"image_{row}_{idx}.jpg"
  525. try:
  526. pil_img = Image.open(io.BytesIO(image_data))
  527. img_format = pil_img.format if pil_img.format else 'jpg'
  528. filename = f"image_{row}_{idx}.{img_format.lower()}"
  529. except:
  530. pass
  531. if row not in image_cache:
  532. image_cache[row] = []
  533. image_cache[row].append({
  534. 'data': image_data,
  535. 'col': col,
  536. 'format': img_format,
  537. 'filename': filename
  538. })
  539. except Exception as e:
  540. self.log(f" ⚠️ 提取图片失败: {str(e)}", "warning")
  541. wb.close()
  542. total = sum(len(imgs) for imgs in image_cache.values())
  543. self.log(f" ✅ 成功提取 {total} 张图片到缓存,分布在 {len(image_cache)} 行", "success")
  544. except Exception as e:
  545. self.log(f" ❌ 提取图片失败: {str(e)}", "error")
  546. return image_cache
  547. def build_request_body(self, row_dict, row_index):
  548. """根据Excel行数据构建请求体"""
  549. try:
  550. name = str(row_dict.get("姓名", "")) if pd.notna(row_dict.get("姓名")) else ""
  551. gender = str(row_dict.get("性别", "")) if pd.notna(row_dict.get("性别")) else ""
  552. phone = str(row_dict.get("手机号码", "")) if pd.notna(row_dict.get("手机号码")) else ""
  553. remark = str(row_dict.get("备注", "")) if pd.notna(row_dict.get("备注")) else ""
  554. institution = str(row_dict.get("机构", "")) if pd.notna(row_dict.get("机构")) else ""
  555. target_school_text = str(row_dict.get("意向学校", "")) if pd.notna(row_dict.get("意向学校")) else ""
  556. consulting_text = str(row_dict.get("咨询项目", "")) if pd.notna(row_dict.get("咨询项目")) else ""
  557. product_text = str(row_dict.get("产品", "")) if pd.notna(row_dict.get("产品")) else ""
  558. source = str(row_dict.get("线索来源", "")) if pd.notna(row_dict.get("线索来源")) else ""
  559. market_person = str(row_dict.get("市场人员", "")) if pd.notna(row_dict.get("市场人员")) else ""
  560. userId = str(row_dict.get("用户ID", "")) if pd.notna(row_dict.get("用户ID")) else ""
  561. channel_code = str(row_dict.get("渠道代码", "")) if pd.notna(row_dict.get("渠道代码")) else ""
  562. userImg = str(row_dict.get("人员头像", "")) if pd.notna(row_dict.get("人员头像")) else ""
  563. phone = phone.split('.')[0]
  564. self.log(f" 📝 提取数据: 姓名={name}, 手机={phone}, 渠道代码={channel_code}, 意向学校={target_school_text}, 咨询项目={consulting_text}, 产品={product_text}", "debug")
  565. school_mapping = {
  566. "上海": "optqw2jaJ5", "徐汇": "optTD4UQsM", "人广": "opt7hQjSZQ",
  567. "长宁": "opt7A8WBtE", "浦东": "optnyoxnTK", "杨浦": "optgArj17r",
  568. "闵行": "optCIwo1J1", "松江": "optuZzXsYm", "锦秋个人学校": "optLQYDW3S",
  569. "封闭学院": "optitJpd5G", "大学生-市区": "optHlvKsKy", "大学生-曹崇杨": "optdcoQXBt",
  570. "大学生-临港": "opt3i4fOKJ", "大学生-松江": "optG7eGFUg", "大学生-奉贤": "optJJqnLgY",
  571. "常州": "opt5wuiYjW"
  572. }
  573. consulting_mapping = {
  574. "语培": "opt96MRele", "留学": "optQFQsXIe", "考研": "opti3wbRF7",
  575. "AF艺术": "optaZC2VCL", "美行音乐": "optzSqwgLp", "锦秋个人学校": "opt9N4AGbJ",
  576. "研学考团": "optdYBLknN", "尚思": "optf8lTDcV"
  577. }
  578. product_mapping = {
  579. "ACT": "optlGkVm5n", "AEAS": "optqX3b1rJ", "ALEVEL": "optgMNcV8D",
  580. "AP": "optWKWYQgo", "BC": "opt8huX4Nv", "DSE": "optxFBYmUs",
  581. "ESL": "optaYj9RZM", "GMAT": "optgwu03Nf", "GRE": "optEOyf5TR",
  582. "IB": "optBhWIw7x", "IGCSE": "optIuoUFpo", "KET": "optjcLdrsB",
  583. "PET": "optY42DNFo", "PTE": "optOrHJvTX", "SAT": "optTxT8NEu",
  584. "SSAT": "optJ2a0tkB", "Toefl Junior": "optEGInE0d", "国际备考": "optgwn1azI",
  585. "国际研学及考团": "optAmxtqGN", "国家地理": "opttXZvoj3", "剑桥英语": "optQkkhzD5",
  586. "锦秋竞赛": "optUgXXmpF", "考研": "optgQrGHTS", "留学预备": "optWxtOU8b",
  587. "美高": "opt36E5u0t", "硕果": "optNKeSnho", "硕果竞赛": "optMzk3TpL",
  588. "腾飞计划": "optYTD9h04", "团企培": "optW5UDF6G", "托福": "optKuZ2zCq",
  589. "雅思": "optWsal1MB", "用英语讲中国故事": "optinda27s", "在线": "opt2X07PY4",
  590. "浙大国际本科": "optZFmVTTC", "语培待定/未知": "optTNitLA5", "大学四级": "optAqYPFmN",
  591. "大学六级": "optpdfKgdl", "菁英树": "optBFhBHFX", "其它": "opt698ww1H"
  592. }
  593. target_school_id = None
  594. if target_school_text:
  595. if target_school_text in school_mapping:
  596. target_school_id = school_mapping[target_school_text]
  597. else:
  598. for school_name, school_id in school_mapping.items():
  599. if school_name in target_school_text or target_school_text in school_name:
  600. target_school_id = school_id
  601. break
  602. consulting_ids = []
  603. if consulting_text:
  604. consulting_items = re.split(r'[,,、/ ]+', consulting_text)
  605. for item in consulting_items:
  606. item = item.strip()
  607. if item and item in consulting_mapping:
  608. consulting_ids.append(consulting_mapping[item])
  609. product_ids = []
  610. if product_text:
  611. product_items = re.split(r'[,,、/ ]+', product_text)
  612. for item in product_items:
  613. item = item.strip()
  614. if item:
  615. if item in product_mapping:
  616. product_ids.append(product_mapping[item])
  617. else:
  618. item_lower = item.lower()
  619. for product_name, product_id in product_mapping.items():
  620. if product_name.lower() == item_lower or product_name in item or item in product_name:
  621. product_ids.append(product_id)
  622. break
  623. remark_text = f"姓名:{name},性别:{gender},手机号码:{phone}"
  624. if remark:
  625. remark_text += f",{remark}"
  626. data_obj = {
  627. "fld43Khnuw": {
  628. "type": 1,
  629. "value": [{"type": "text", "text": remark_text}]
  630. },
  631. "fldexDPwcE": {"type": 3, "value": "optmlZt00z"} if institution else {"type": 3},
  632. "fldWt6JrgK": {"type": 3, "value": target_school_id} if target_school_id else {"type": 3},
  633. "fldirDDoLV": {"type": 4, "value": consulting_ids} if consulting_ids else {"type": 4},
  634. "fldudfcNVO": {"type": 4, "value": product_ids} if product_ids else {"type": 4},
  635. "fldS90QCXo": {"type": 1, "value": [{"type": "text", "text": name}]} if name else {"type": 1},
  636. "fldsA4YdS6": {"type": 3},
  637. "fldSSElgkA": {"type": 13, "value": {"fullPhoneNum": phone}} if phone else {"type": 13},
  638. "fldZPL52gS": {"type": 1},
  639. "fldPIaD4UH": {"type": 13},
  640. "fldUGeDVDF": {"type": 13},
  641. "fldKZ5WKyN": {"type": 1},
  642. "fld0MHPEaW": {"type": 3},
  643. "fldOLvk3zZ": {"type": 3},
  644. "fldkSVVnuu": {"type": 1},
  645. "fldCfRL3qM": {"type": 3, "value": "optTWIvAFb"} if source else {"type": 3},
  646. "fldodpd0fR": {
  647. "type": 11,
  648. "value": {
  649. "users": [{
  650. "userId": userId,
  651. "name": market_person,
  652. "avatarUrl": userImg,
  653. "enName": market_person,
  654. "notify": False,
  655. }]
  656. }
  657. } if market_person else {"type": 11},
  658. "flde7thxT7": {"type": 3, "value": channel_code},
  659. "fldaxavu52": {"type": 3, "value": "optHsn4zKU"},
  660. "fldMpwPaI1": {"type": 17}
  661. }
  662. if self.image_cache and row_index in self.image_cache:
  663. images = self.image_cache[row_index]
  664. if images:
  665. img_info = images[0]
  666. if row_index in self.uploaded_cache:
  667. img_value = self.uploaded_cache[row_index]
  668. else:
  669. uploaded = self.upload_image(img_info['data'], img_info['filename'])
  670. if uploaded:
  671. self.uploaded_cache[row_index] = uploaded
  672. img_value = uploaded
  673. else:
  674. img_value = None
  675. if img_value:
  676. data_obj["fldMpwPaI1"] = {
  677. "type": 17,
  678. "value": [img_value]
  679. }
  680. body = {
  681. "shareToken": self.share_token,
  682. "data": json.dumps(data_obj, ensure_ascii=False),
  683. "preUploadEnable": True
  684. }
  685. return body
  686. except Exception as e:
  687. self.log(f"构建请求体失败: {str(e)}", "error")
  688. import traceback
  689. self.log(f"详细错误: {traceback.format_exc()}", "debug")
  690. return None
  691. def send_request(self, row_data, row_index):
  692. """发送单条请求"""
  693. try:
  694. self.log(f"\n{'='*50}", "info")
  695. self.log(f"📤 准备发送第 {row_index} 行数据", "info")
  696. cookie_string = self.cookie_text.get(1.0, tk.END).strip()
  697. if not cookie_string:
  698. self.log("❌ Cookie为空,请先配置Cookie", "error")
  699. return False, "Cookie为空"
  700. body = self.build_request_body(row_data, row_index)
  701. if body is None:
  702. self.log("❌ 构建请求体失败", "error")
  703. return False, "构建请求体失败"
  704. body_str = json.dumps(body, ensure_ascii=False)
  705. self.log(f"📦 请求体大小: {len(body_str)} 字节", "debug")
  706. headers = self.default_headers.copy()
  707. cookies = self.parse_cookie(cookie_string)
  708. headers["x-csrftoken"] = self._get_csrf_token()
  709. headers["f-version"] = "docs-6-17-1781628518399"
  710. response = requests.post(
  711. self.url,
  712. headers=headers,
  713. cookies=cookies,
  714. json=body,
  715. timeout=30
  716. )
  717. try:
  718. resp_data = response.json()
  719. resp_text = json.dumps(resp_data, ensure_ascii=False, indent=2)
  720. except:
  721. resp_text = response.text
  722. if response.status_code == 200:
  723. self.log(f"✅ 第{row_index}行 发送成功", "success")
  724. return True, resp_text
  725. else:
  726. self.log(f"❌ 第{row_index}行 发送失败 (HTTP {response.status_code})", "error")
  727. return False, resp_text
  728. except requests.exceptions.Timeout:
  729. self.log(f"⏱️ 第{row_index}行 请求超时", "error")
  730. return False, "请求超时"
  731. except requests.exceptions.ConnectionError:
  732. self.log(f"🔌 第{row_index}行 连接失败", "error")
  733. return False, "连接失败"
  734. except Exception as e:
  735. self.log(f"💥 第{row_index}行 异常: {str(e)}", "error")
  736. return False, str(e)
  737. finally:
  738. self.log(f"{'='*50}\n", "info")
  739. def send_all(self):
  740. """发送所有数据的主循环(已增加暂停和阈值检查)"""
  741. try:
  742. excel_file = self.file_path_var.get()
  743. sheet_name = self.sheet_var.get()
  744. min_interval = float(self.min_interval_var.get())
  745. max_interval = float(self.max_interval_var.get())
  746. start_row = int(self.start_row_var.get()) - 1
  747. if not excel_file:
  748. self.log("❌ 请先选择Excel文件", "error")
  749. self.status_var.set("❌ 请先选择Excel文件")
  750. return
  751. # 初始化计数
  752. self.sent_count = 0
  753. self.count_display_var.set("本次已发送: 0 条")
  754. self.uploaded_cache = {}
  755. self.log(f"🔍 正在自动提取Excel中的嵌入图片...", "info")
  756. self.image_cache = self.extract_images_to_cache(excel_file, sheet_name)
  757. if self.image_cache:
  758. total_images = sum(len(imgs) for imgs in self.image_cache.values())
  759. self.log(f"📊 共提取 {total_images} 张图片,分布在 {len(self.image_cache)} 行", "info")
  760. else:
  761. self.log(f"📷 未检测到任何嵌入图片", "info")
  762. try:
  763. df = pd.read_excel(excel_file, sheet_name=sheet_name)
  764. self.log(f"📊 成功读取Excel: {len(df)} 行数据", "info")
  765. except Exception as e:
  766. self.log(f"❌ 读取Excel失败: {str(e)}", "error")
  767. self.status_var.set(f"❌ 读取失败: {str(e)}")
  768. return
  769. if len(df) == 0:
  770. self.log("⚠️ Excel中没有数据", "warning")
  771. return
  772. df = df.iloc[start_row:]
  773. total = len(df)
  774. success_count = 0
  775. fail_count = 0
  776. self.log(f"🚀 开始发送,共 {total} 条数据", "info")
  777. self.status_var.set(f"🔄 发送中... 0/{total}")
  778. for idx, (_, row) in enumerate(df.iterrows()):
  779. # ========== 暂停检查(核心新增)==========
  780. while self.is_paused and not self.stop_flag:
  781. self.status_var.set(f"⏸ 已暂停 | 成功:{success_count} 失败:{fail_count}")
  782. time.sleep(0.5)
  783. if self.stop_flag:
  784. self.log("⏹️ 已停止发送", "warning")
  785. self.status_var.set("⏹️ 已停止")
  786. break
  787. excel_row_number = idx + 2 + start_row
  788. if self.image_cache and excel_row_number in self.image_cache:
  789. img_count = len(self.image_cache[excel_row_number])
  790. self.log(f" 🖼️ 第{excel_row_number}行有 {img_count} 个嵌入图片", "info")
  791. success, resp_text = self.send_request(row.to_dict(), excel_row_number)
  792. if success:
  793. success_count += 1
  794. self.sent_count += 1
  795. self.count_display_var.set(f"本次已发送: {self.sent_count} 条")
  796. # ========== 阈值检查(核心新增)==========
  797. if self._check_threshold():
  798. self.log(f"⏸ 因达到阈值暂停,等待手动继续...", "pause")
  799. # 进入暂停等待循环
  800. while self.is_paused and not self.stop_flag:
  801. self.status_var.set(f"⏸ 已达阈值暂停 | 成功:{success_count} 失败:{fail_count}")
  802. time.sleep(0.5)
  803. if self.stop_flag:
  804. break
  805. # 继续后重置计数(可选:如果想累计不清零,注释掉下面这行)
  806. # self.sent_count = 0
  807. # self.count_display_var.set(f"本次已发送: {self.sent_count} 条")
  808. else:
  809. fail_count += 1
  810. self.status_var.set(f"🔄 发送中... {idx+1}/{total} (成功:{success_count} 失败:{fail_count})")
  811. if idx < total - 1 and not self.stop_flag:
  812. interval = random.uniform(min_interval, max_interval)
  813. self.log(f"⏳ 等待 {interval:.1f} 秒...", "info")
  814. time.sleep(interval)
  815. self.log(f"🏁 发送完成!成功: {success_count}, 失败: {fail_count}", "success" if fail_count == 0 else "warning")
  816. self.status_var.set(f"✅ 完成 成功:{success_count} 失败:{fail_count}")
  817. except Exception as e:
  818. self.log(f"💥 程序异常: {str(e)}", "error")
  819. import traceback
  820. self.log(f"详细错误: {traceback.format_exc()}", "debug")
  821. self.status_var.set(f"❌ 异常: {str(e)}")
  822. finally:
  823. self.is_running = False
  824. self.is_paused = False
  825. self.pause_btn.config(text="⏸ 暂停", state=tk.DISABLED)
  826. self.start_btn.config(state=tk.NORMAL)
  827. self.stop_btn.config(state=tk.DISABLED)
  828. def start_send(self):
  829. """启动发送线程"""
  830. if self.is_running:
  831. return
  832. if not self.file_path_var.get():
  833. messagebox.showwarning("提示", "请先选择Excel文件")
  834. return
  835. cookie = self.cookie_text.get(1.0, tk.END).strip()
  836. if not cookie:
  837. messagebox.showwarning("提示", "请先配置Cookie")
  838. return
  839. try:
  840. float(self.min_interval_var.get())
  841. float(self.max_interval_var.get())
  842. int(self.start_row_var.get())
  843. threshold_str = self.threshold_var.get().strip()
  844. if threshold_str and threshold_str != "0":
  845. int(threshold_str)
  846. except ValueError:
  847. messagebox.showerror("错误", "间隔、行号或阈值请输入有效数字")
  848. return
  849. # 验证时间格式
  850. for time_var, name in [(self.start_time_var, "启动时间"), (self.stop_time_var, "关闭时间")]:
  851. t = time_var.get().strip()
  852. if t:
  853. if not re.match(r'^([01]\d|2[0-3]):([0-5]\d)$', t):
  854. messagebox.showerror("错误", f"{name} 格式错误,请使用 HH:MM 格式")
  855. return
  856. self.is_running = True
  857. self.stop_flag = False
  858. self.is_paused = False
  859. self.start_btn.config(state=tk.DISABLED)
  860. self.stop_btn.config(state=tk.NORMAL)
  861. self.pause_btn.config(text="⏸ 暂停", state=tk.NORMAL)
  862. self.status_var.set("🔄 正在发送...")
  863. thread = threading.Thread(target=self.send_all, daemon=True)
  864. thread.start()
  865. def stop_send(self):
  866. """停止发送"""
  867. self.stop_flag = True
  868. self.is_paused = False
  869. self.log("⏹️ 正在停止...", "warning")
  870. self.stop_btn.config(state=tk.DISABLED)
  871. self.pause_btn.config(state=tk.DISABLED)
  872. def extract_images_to_folder(self, file_path, sheet_name, output_folder=None):
  873. """提取Excel中的所有图片并保存到文件夹"""
  874. try:
  875. if output_folder is None:
  876. base_dir = os.path.dirname(file_path)
  877. base_name = os.path.splitext(os.path.basename(file_path))[0]
  878. output_folder = os.path.join(base_dir, f"{base_name}_提取的图片")
  879. if not os.path.exists(output_folder):
  880. os.makedirs(output_folder)
  881. mapping_file = os.path.join(output_folder, "图片映射表.txt")
  882. wb = load_workbook(file_path, data_only=True)
  883. ws = wb[sheet_name]
  884. image_count = 0
  885. error_count = 0
  886. with open(mapping_file, 'w', encoding='utf-8') as f:
  887. f.write("="*60 + "\n")
  888. f.write("Excel图片提取映射表\n")
  889. f.write(f"源文件: {file_path}\n")
  890. f.write(f"工作表: {sheet_name}\n")
  891. f.write(f"提取时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
  892. f.write("="*60 + "\n\n")
  893. if hasattr(ws, '_images') and ws._images:
  894. for idx, img in enumerate(ws._images):
  895. try:
  896. if hasattr(img, 'anchor') and hasattr(img.anchor, '_from'):
  897. anchor_from = img.anchor._from
  898. row = anchor_from.row + 1
  899. col = anchor_from.col + 1
  900. col_letter = get_column_letter(col)
  901. else:
  902. row = idx + 1
  903. col = 0
  904. col_letter = '?'
  905. image_data = None
  906. if hasattr(img, '_data') and callable(img._data):
  907. image_data = img._data()
  908. elif hasattr(img, '_data') and not callable(img._data):
  909. image_data = img._data
  910. elif hasattr(img, 'image'):
  911. if hasattr(img.image, '_data') and callable(img.image._data):
  912. image_data = img.image._data()
  913. elif hasattr(img.image, '_data'):
  914. image_data = img.image._data
  915. if image_data is None:
  916. error_count += 1
  917. continue
  918. img_format = 'jpg'
  919. width, height = 0, 0
  920. try:
  921. pil_img = Image.open(io.BytesIO(image_data))
  922. img_format = pil_img.format if pil_img.format else 'jpg'
  923. width, height = pil_img.size
  924. filename = f"第{row}行_列{col_letter}_{idx+1}.{img_format.lower()}"
  925. filepath = os.path.join(output_folder, filename)
  926. pil_img.save(filepath)
  927. except:
  928. filename = f"第{row}行_列{col_letter}_{idx+1}.jpg"
  929. filepath = os.path.join(output_folder, filename)
  930. with open(filepath, 'wb') as f_out:
  931. f_out.write(image_data)
  932. image_count += 1
  933. except Exception as e:
  934. error_count += 1
  935. wb.close()
  936. self.log(f"✅ 成功提取 {image_count} 张图片到: {output_folder}", "success")
  937. return output_folder
  938. except Exception as e:
  939. self.log(f"❌ 提取图片失败: {str(e)}", "error")
  940. return None
  941. def extract_images(self):
  942. """手动提取图片的入口方法"""
  943. excel_file = self.file_path_var.get()
  944. if not excel_file:
  945. messagebox.showwarning("提示", "请先选择Excel文件")
  946. return
  947. sheet_name = self.sheet_var.get()
  948. if not messagebox.askyesno("确认",
  949. f"即将提取Excel中的所有图片\n\n"
  950. f"文件: {os.path.basename(excel_file)}\n"
  951. f"工作表: {sheet_name}\n\n"
  952. f"是否继续?"):
  953. return
  954. def extract_thread():
  955. self.log("📸 开始提取图片...", "info")
  956. self.status_var.set("🔄 正在提取图片...")
  957. self.start_btn.config(state=tk.DISABLED)
  958. output_folder = self.extract_images_to_folder(excel_file, sheet_name)
  959. if output_folder:
  960. self.status_var.set(f"✅ 图片已提取到: {output_folder}")
  961. messagebox.showinfo("提取成功",
  962. f"图片已成功提取!\n\n"
  963. f"保存位置: {output_folder}\n\n"
  964. f"请查看文件夹中的 '图片映射表.txt' 文件")
  965. else:
  966. self.status_var.set("❌ 提取失败")
  967. messagebox.showerror("提取失败", "图片提取失败,请查看日志了解详情。")
  968. self.start_btn.config(state=tk.NORMAL)
  969. thread = threading.Thread(target=extract_thread, daemon=True)
  970. thread.start()
  971. if __name__ == "__main__":
  972. root = tk.Tk()
  973. app = FeishuSubmitApp(root)
  974. root.mainloop()