| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677 |
- import ctypes
- import time
- import os
- import sys
- import platform
- import win32gui
- import win32con
- import win32api
- import win32clipboard as clipboard
- from PIL import ImageGrab
- import requests
- import io
- import tkinter as tk
- from tkinter import ttk, scrolledtext, messagebox
- import threading
- import json
- lastKeyWord = ""
- # ========== 获取程序目录 ==========
- def get_app_dir():
- if getattr(sys, 'frozen', False):
- return os.path.dirname(sys.executable)
- else:
- return os.path.dirname(os.path.abspath(__file__))
- APP_DIR = get_app_dir()
- # ========== 全局变量 ==========
- is_running = False
- stop_flag = False
- config_file = os.path.join(APP_DIR, "config.json")
- # ========== 幽灵键鼠加载 ==========
- def load_ghost_key_mouse():
- if platform.architecture()[0] == "64bit":
- dll_path = os.path.join(APP_DIR, "gbild64.dll")
- else:
- dll_path = os.path.join(APP_DIR, "gbild32.dll")
-
- if not os.path.exists(dll_path):
- return None
-
- try:
- dll = ctypes.windll.LoadLibrary(dll_path)
- return dll
- except Exception as e:
- return None
- ghost_dll = load_ghost_key_mouse()
- if ghost_dll:
- ghost_dll.getmodel.restype = ctypes.c_char_p
- ghost_dll.getserialnumber.restype = ctypes.c_char_p
- ghost_dll.getproductiondate.restype = ctypes.c_char_p
- ghost_dll.getfirmwareversion.restype = ctypes.c_char_p
- ghost_dll.getclientscreenresolution.restype = ctypes.c_char_p
- ghost_dll.readstring.restype = ctypes.c_char_p
- ghost_dll.encryptstring.restype = ctypes.c_char_p
- ghost_dll.decryptstring.restype = ctypes.c_char_p
- ghost_dll.getproductname.restype = ctypes.c_char_p
- ghost_dll.sdktype.restype = ctypes.c_char_p
- ghost_dll.sdkversion.restype = ctypes.c_char_p
- def opendevice(index=0):
- return ghost_dll.opendevice(index)
- def closedevice():
- return ghost_dll.closedevice()
- def pressandreleasemousebutton(mbtn):
- return ghost_dll.pressandreleasemousebutton(mbtn)
- def combinationkey(key_sequence):
- return ghost_dll.combinationkey(key_sequence)
- def pressandreleasekeybyname(key_name):
- return ghost_dll.pressandreleasekeybyname(key_name)
- device_id = opendevice(0)
- if device_id == 0:
- ghost_available = False
- else:
- ghost_available = True
- else:
- ghost_available = False
- # ========== 核心功能类 ==========
- class AuctionBot:
- def __init__(self, log_callback):
- self.log_callback = log_callback
- self.target_hwnd = None
- self.left = 0
- self.top = 0
- self.is_running = False
-
- def log(self, message):
- if self.log_callback:
- self.log_callback(message)
-
- def get_hwnds_by_title_contains(self, title_contains):
- hwnds = []
- def enum_callback(hwnd, _):
- if win32gui.IsWindowVisible(hwnd):
- window_title = win32gui.GetWindowText(hwnd)
- if title_contains in window_title:
- hwnds.append(hwnd)
- return True
- win32gui.EnumWindows(enum_callback, None)
- return hwnds
- def get_client_rect(self, hwnd):
- rect = win32gui.GetClientRect(hwnd)
- point = win32gui.ClientToScreen(hwnd, (rect[0], rect[1]))
- return (point[0], point[1], point[0] + rect[2], point[1] + rect[3])
- def activate_window(self, hwnd):
- try:
- if win32gui.IsIconic(hwnd):
- win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
- win32gui.SetForegroundWindow(hwnd)
- win32gui.BringWindowToTop(hwnd)
- time.sleep(0.2)
- return True
- except Exception as e:
- self.log(f"激活窗口失败: {e}")
- return False
- def capture_window_area_to_bytes(self, hwnd, x1, y1, x2, y2):
- try:
- client_rect = self.get_client_rect(hwnd)
- left, top, right, bottom = client_rect
-
- screen_x1 = left + x1
- screen_y1 = top + y1
- screen_x2 = left + x2
- screen_y2 = top + y2
-
- screenshot = ImageGrab.grab(bbox=(screen_x1, screen_y1, screen_x2, screen_y2))
- img_bytes = io.BytesIO()
- screenshot.save(img_bytes, format='PNG')
- img_bytes.seek(0)
- return img_bytes.getvalue()
- except Exception as e:
- self.log(f"截图失败: {e}")
- return None
- def ocr_price(self, image_bytes):
- try:
- # url = "http://111.229.45.38:19100/ocr"
- url = "http://192.168.1.101:10001/ocr"
- files = {'image': ('screenshot.png', image_bytes, 'image/png')}
-
- self.log("正在识别价格...")
- response = requests.post(url, files=files, timeout=30)
-
- if response.status_code != 200:
- self.log(f"OCR请求失败: HTTP {response.status_code}")
- return None
-
- result = response.json()
-
- if not result.get('success', False):
- self.log(f"OCR识别失败: {result}")
- return None
-
- texts = result.get('texts', [])
- if not texts:
- self.log("未识别到任何文字")
- return None
-
- all_texts = []
- for item in texts:
- rec_texts = item.get('rec_texts', [])
- all_texts.extend(rec_texts)
-
- for text in all_texts:
- try:
- price = float(text)
- self.log(f"识别到价格: {price}")
- return price
- except ValueError:
- continue
-
- self.log(f"未识别到价格,识别到的文字: {all_texts}")
- return None
- except Exception as e:
- self.log(f"OCR识别异常: {e}")
- return None
- def mouse_click_at(self, x, y, button=0):
- try:
- # 1. 移动物理光标(部分游戏会校验系统光标位置)
- win32api.SetCursorPos((x, y))
- time.sleep(0.1)
- # 2. 准备 SendInput 结构体(64/32位兼容)
- ULONG_PTR = ctypes.c_uint64 if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_uint32
- class MOUSEINPUT(ctypes.Structure):
- _fields_ = [
- ("dx", ctypes.c_long),
- ("dy", ctypes.c_long),
- ("mouseData", ctypes.c_ulong),
- ("dwFlags", ctypes.c_ulong),
- ("time", ctypes.c_ulong),
- ("dwExtraInfo", ULONG_PTR),
- ]
- class KEYBDINPUT(ctypes.Structure):
- _fields_ = [
- ("wVk", ctypes.c_ushort),
- ("wScan", ctypes.c_ushort),
- ("dwFlags", ctypes.c_ulong),
- ("time", ctypes.c_ulong),
- ("dwExtraInfo", ULONG_PTR),
- ]
- class HARDWAREINPUT(ctypes.Structure):
- _fields_ = [
- ("uMsg", ctypes.c_ulong),
- ("wParamL", ctypes.c_ushort),
- ("wParamH", ctypes.c_ushort),
- ]
- class DUMMYUNIONNAME(ctypes.Union):
- _fields_ = [("mi", MOUSEINPUT), ("ki", KEYBDINPUT), ("hi", HARDWAREINPUT)]
- class INPUT(ctypes.Structure):
- _fields_ = [("type", ctypes.c_ulong), ("DUMMYUNIONNAME", DUMMYUNIONNAME)]
- INPUT_MOUSE = 0
- screen_w = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
- screen_h = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
- # 3. 转换为绝对坐标(0-65535)
- abs_x = int(x * 65535 / (screen_w - 1)) if screen_w > 1 else 0
- abs_y = int(y * 65535 / (screen_h - 1)) if screen_h > 1 else 0
- # 4. 选择按键
- if button == 0:
- down_flag = win32con.MOUSEEVENTF_LEFTDOWN
- up_flag = win32con.MOUSEEVENTF_LEFTUP
- else:
- down_flag = win32con.MOUSEEVENTF_RIGHTDOWN
- up_flag = win32con.MOUSEEVENTF_RIGHTUP
- move_abs = win32con.MOUSEEVENTF_ABSOLUTE | win32con.MOUSEEVENTF_MOVE
- # 5. 按下(带绝对坐标移动)
- inp_down = INPUT()
- inp_down.type = INPUT_MOUSE
- inp_down.DUMMYUNIONNAME.mi.dx = abs_x
- inp_down.DUMMYUNIONNAME.mi.dy = abs_y
- inp_down.DUMMYUNIONNAME.mi.mouseData = 0
- inp_down.DUMMYUNIONNAME.mi.dwFlags = move_abs | down_flag
- inp_down.DUMMYUNIONNAME.mi.time = 0
- inp_down.DUMMYUNIONNAME.mi.dwExtraInfo = 0
- ctypes.windll.user32.SendInput(1, ctypes.byref(inp_down), ctypes.sizeof(INPUT))
-
- # 关键:保持按下状态至少100ms,确保游戏能轮询到
- time.sleep(0.1)
- # 6. 释放
- inp_up = INPUT()
- inp_up.type = INPUT_MOUSE
- inp_up.DUMMYUNIONNAME.mi.dx = abs_x
- inp_up.DUMMYUNIONNAME.mi.dy = abs_y
- inp_up.DUMMYUNIONNAME.mi.mouseData = 0
- inp_up.DUMMYUNIONNAME.mi.dwFlags = move_abs | up_flag
- inp_up.DUMMYUNIONNAME.mi.time = 0
- inp_up.DUMMYUNIONNAME.mi.dwExtraInfo = 0
- ctypes.windll.user32.SendInput(1, ctypes.byref(inp_up), ctypes.sizeof(INPUT))
- time.sleep(0.1)
- return True
- except Exception as e:
- self.log(f"鼠标点击失败: {e}")
- return False
- def keyboard_ctrl_v(self):
- try:
- if ghost_available:
- return combinationkey(b"ctrl+v") == 1
- else:
- win32api.keybd_event(win32con.VK_CONTROL, 0, 0, 0)
- time.sleep(0.05)
- win32api.keybd_event(ord('V'), 0, 0, 0)
- time.sleep(0.05)
- win32api.keybd_event(ord('V'), 0, win32con.KEYEVENTF_KEYUP, 0)
- time.sleep(0.05)
- win32api.keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0)
- return True
- except Exception as e:
- self.log(f"Ctrl+V失败: {e}")
- return False
- def clipboard_set_text(self, text):
- try:
- clipboard.OpenClipboard()
- clipboard.EmptyClipboard()
- clipboard.SetClipboardText(text, clipboard.CF_TEXT)
- clipboard.CloseClipboard()
- return True
- except Exception as e:
- self.log(f"设置剪贴板失败: {e}")
- return False
- def process_item(self, keyword, target_price):
- global lastKeyWord
- """处理单个关键词"""
- if stop_flag:
- return False
-
- self.log(f"========== 处理: {keyword} (目标价格: {target_price}) ==========")
- if (lastKeyWord != keyword):
- # 点击重置
- click_x = self.left + 728
- click_y = self.top + 152
- self.log(f"点击重置: ({click_x}, {click_y})")
- self.mouse_click_at(click_x, click_y, 0)
- time.sleep(1)
-
- # 设置剪贴板
- self.log(f"设置剪贴板: '{keyword}'")
- self.clipboard_set_text(keyword)
-
- # 点击输入框粘贴
- click_x = self.left + 568
- click_y = self.top + 152
- self.log(f"点击输入框: ({click_x}, {click_y})")
- self.mouse_click_at(click_x, click_y, 0)
- time.sleep(1)
-
- # Ctrl+V
- self.log("执行 Ctrl+V")
- self.keyboard_ctrl_v()
- time.sleep(1)
- else:
- self.log(f"搜索关键词一样,自动忽略重置")
- lastKeyWord = keyword
- # 点击搜索按钮
- click_x = self.left + 664
- click_y = self.top + 152
- self.log(f"点击搜索按钮: ({click_x}, {click_y})")
- self.mouse_click_at(click_x, click_y, 0)
- time.sleep(2)
-
- # 截图识别价格
- self.log("截图识别价格...")
- img_bytes = self.capture_window_area_to_bytes(self.target_hwnd, 580, 190 - 30, 634, 211 - 30)
-
- if not img_bytes:
- self.log("截图失败")
- return False
-
- price = self.ocr_price(img_bytes)
-
- if price is None:
- self.log("价格识别失败")
- return False
-
- self.log(f"识别到价格: {price}, 目标价格: {target_price}")
-
- if price <= target_price and price > 1:
- self.log(f"价格 {price} <= {target_price},执行购买!")
-
- # 选择购买项
- click_x = self.left + 506
- click_y = self.top + 202
- self.log(f"选择购买项: ({click_x}, {click_y})")
- self.mouse_click_at(click_x, click_y, 0)
- time.sleep(0.5)
-
- # 购买
- click_x = self.left + 729
- click_y = self.top + 545
- self.log(f"点击购买: ({click_x}, {click_y})")
- self.mouse_click_at(click_x, click_y, 0)
- time.sleep(1)
-
- # 确认购买
- click_x = self.left + 516
- click_y = self.top + 286
- self.log(f"确认购买: ({click_x}, {click_y})")
- self.mouse_click_at(click_x, click_y, 0)
-
- self.log(f"✅ {keyword} 购买成功! 价格: {price}")
- return True
- else:
- self.log(f"价格 {price} > {target_price},不购买")
- return False
- def run(self, keywords_prices, interval):
- """主循环"""
- global stop_flag
- stop_flag = False
-
- self.log("="*60)
- self.log("开始自动拍卖操作")
- self.log("="*60)
-
- # 查找窗口
- self.log("查找新天龙八部窗口...")
- window_list = self.get_hwnds_by_title_contains("新天龙八部")
-
- if not window_list:
- self.log("未找到新天龙八部窗口!")
- return
-
- self.target_hwnd = window_list[0]
- window_title = win32gui.GetWindowText(self.target_hwnd)
- self.log(f"找到窗口: {window_title}")
-
- # 获取窗口位置
- client_rect = self.get_client_rect(self.target_hwnd)
- self.left, self.top, right, bottom = client_rect
- self.top = self.top - 30
- self.log(f"窗口位置: left={self.left}, top={self.top}")
-
- # 激活窗口
- self.log("激活窗口...")
- self.activate_window(self.target_hwnd)
- time.sleep(1)
-
- round_count = 0
-
- while not stop_flag:
- round_count += 1
- self.log(f"\n========== 第 {round_count} 轮 ==========")
-
- for keyword, target_price in keywords_prices:
- if stop_flag:
- break
-
- try:
- self.process_item(keyword, target_price)
- time.sleep(1)
- except Exception as e:
- self.log(f"处理 {keyword} 时出错: {e}")
-
- if not stop_flag:
- self.log(f"等待 {interval} 秒后继续...")
- # 分段等待,便于检测停止信号
- for _ in range(interval):
- if stop_flag:
- break
- time.sleep(1)
-
- self.log("程序已停止")
- # ========== GUI程序 ==========
- class AuctionGUI:
- def __init__(self, root):
- self.root = root
- self.root.title("天龙八部自动拍卖机器人")
- self.root.geometry("800x700")
- self.root.resizable(True, True)
-
- self.bot = AuctionBot(self.log_message)
- self.keywords_list = []
- self.running = False
-
- self.setup_ui()
- self.load_config()
-
- def setup_ui(self):
- # 主框架
- main_frame = ttk.Frame(self.root, padding="10")
- main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
-
- # 配置区域
- config_frame = ttk.LabelFrame(main_frame, text="配置", padding="10")
- config_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
-
- # 关键词列表
- ttk.Label(config_frame, text="关键词和价格:").grid(row=0, column=0, sticky=tk.W)
-
- # 关键词表格
- columns = ('关键词', '目标价格')
- self.tree = ttk.Treeview(config_frame, columns=columns, show='headings', height=6)
- self.tree.heading('关键词', text='关键词')
- self.tree.heading('目标价格', text='目标价格')
- self.tree.column('关键词', width=150)
- self.tree.column('目标价格', width=100)
- self.tree.grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(5, 5))
-
- # 滚动条
- scrollbar = ttk.Scrollbar(config_frame, orient=tk.VERTICAL, command=self.tree.yview)
- scrollbar.grid(row=1, column=4, sticky=(tk.N, tk.S))
- self.tree.configure(yscrollcommand=scrollbar.set)
-
- # 添加关键词
- ttk.Label(config_frame, text="关键词:").grid(row=2, column=0, sticky=tk.W, pady=(5, 0))
- self.keyword_entry = ttk.Entry(config_frame, width=15)
- self.keyword_entry.grid(row=3, column=0, sticky=tk.W, pady=(0, 5))
-
- ttk.Label(config_frame, text="目标价格:").grid(row=2, column=1, sticky=tk.W, pady=(5, 0))
- self.price_entry = ttk.Entry(config_frame, width=10)
- self.price_entry.grid(row=3, column=1, sticky=tk.W, pady=(0, 5))
-
- ttk.Button(config_frame, text="添加", command=self.add_keyword).grid(row=3, column=2, padx=(5, 0))
- ttk.Button(config_frame, text="删除选中", command=self.delete_keyword).grid(row=3, column=3, padx=(5, 0))
-
- # 循环间隔
- ttk.Label(config_frame, text="循环间隔(秒):").grid(row=4, column=0, sticky=tk.W, pady=(5, 0))
- self.interval_var = tk.StringVar(value="10")
- self.interval_entry = ttk.Entry(config_frame, textvariable=self.interval_var, width=10)
- self.interval_entry.grid(row=5, column=0, sticky=tk.W, pady=(0, 5))
-
- # 控制按钮
- control_frame = ttk.Frame(config_frame)
- control_frame.grid(row=5, column=1, columnspan=3, sticky=tk.E, pady=(0, 5))
-
- self.start_btn = ttk.Button(control_frame, text="开始", command=self.start_bot)
- self.start_btn.grid(row=0, column=0, padx=(0, 5))
-
- self.stop_btn = ttk.Button(control_frame, text="停止", command=self.stop_bot, state=tk.DISABLED)
- self.stop_btn.grid(row=0, column=1, padx=(0, 5))
-
- ttk.Button(control_frame, text="保存配置", command=self.save_config).grid(row=0, column=2, padx=(0, 5))
- ttk.Button(control_frame, text="加载配置", command=self.load_config).grid(row=0, column=3)
-
- # 日志区域
- log_frame = ttk.LabelFrame(main_frame, text="日志", padding="10")
- log_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
-
- self.log_text = scrolledtext.ScrolledText(log_frame, height=20, width=80)
- self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
-
- # 配置网格权重
- self.root.columnconfigure(0, weight=1)
- self.root.rowconfigure(0, weight=1)
- main_frame.columnconfigure(0, weight=1)
- main_frame.rowconfigure(1, weight=1)
- log_frame.columnconfigure(0, weight=1)
- log_frame.rowconfigure(0, weight=1)
-
- def add_keyword(self):
- keyword = self.keyword_entry.get().strip()
- price_str = self.price_entry.get().strip()
-
- if not keyword:
- messagebox.showwarning("警告", "请输入关键词")
- return
-
- try:
- price = float(price_str)
- except ValueError:
- messagebox.showwarning("警告", "请输入有效的价格")
- return
-
- self.tree.insert('', 'end', values=(keyword, price))
- self.keyword_entry.delete(0, tk.END)
- self.price_entry.delete(0, tk.END)
-
- def delete_keyword(self):
- selected = self.tree.selection()
- if not selected:
- messagebox.showwarning("警告", "请先选中要删除的项目")
- return
- for item in selected:
- self.tree.delete(item)
-
- def get_keywords_list(self):
- items = self.tree.get_children()
- result = []
- for item in items:
- values = self.tree.item(item)['values']
- if values:
- result.append((values[0], float(values[1])))
- return result
-
- def log_message(self, message):
- self.log_text.insert(tk.END, f"{time.strftime('%H:%M:%S')} - {message}\n")
- self.log_text.see(tk.END)
- self.root.update_idletasks()
-
- def start_bot(self):
- keywords = self.get_keywords_list()
- if not keywords:
- messagebox.showwarning("警告", "请至少添加一个关键词")
- return
-
- try:
- interval = int(self.interval_var.get())
- if interval < 1:
- raise ValueError
- except ValueError:
- messagebox.showwarning("警告", "请输入有效的间隔秒数(大于0)")
- return
-
- if not ghost_available:
- result = messagebox.askyesno("警告", "幽灵键鼠未连接,点击将使用系统API,是否继续?")
- if not result:
- return
-
- self.running = True
- self.start_btn.config(state=tk.DISABLED)
- self.stop_btn.config(state=tk.NORMAL)
-
- # 在新线程中运行
- self.bot_thread = threading.Thread(
- target=self.bot.run,
- args=(keywords, interval)
- )
- self.bot_thread.daemon = True
- self.bot_thread.start()
-
- def stop_bot(self):
- global stop_flag
- stop_flag = True
- self.log_message("正在停止...")
- self.start_btn.config(state=tk.NORMAL)
- self.stop_btn.config(state=tk.DISABLED)
- self.running = False
-
- def save_config(self):
- keywords = self.get_keywords_list()
- config = {
- 'keywords': keywords,
- 'interval': self.interval_var.get()
- }
- try:
- with open(config_file, 'w', encoding='utf-8') as f:
- json.dump(config, f, ensure_ascii=False, indent=2)
- self.log_message(f"配置已保存到: {config_file}")
- messagebox.showinfo("成功", "配置已保存")
- except Exception as e:
- messagebox.showerror("错误", f"保存配置失败: {e}")
-
- def load_config(self):
- try:
- if not os.path.exists(config_file):
- return
-
- with open(config_file, 'r', encoding='utf-8') as f:
- config = json.load(f)
-
- # 清空当前列表
- for item in self.tree.get_children():
- self.tree.delete(item)
-
- # 加载关键词
- for keyword, price in config.get('keywords', []):
- self.tree.insert('', 'end', values=(keyword, price))
-
- # 加载间隔
- if 'interval' in config:
- self.interval_var.set(config['interval'])
-
- self.log_message("配置已加载")
- except Exception as e:
- messagebox.showerror("错误", f"加载配置失败: {e}")
- def main():
- root = tk.Tk()
- app = AuctionGUI(root)
- root.mainloop()
- if __name__ == "__main__":
- try:
- main()
- except Exception as e:
- print(f"程序出错: {e}")
- import traceback
- traceback.print_exc()
- finally:
- if ghost_available:
- try:
- closedevice()
- except:
- pass
|