| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- Cursor自动聊天工具
- 这个工具可以自动查找Cursor窗口,定位到chat窗口,并自动发送消息。
- 功能:
- 1. 查找Windows操作系统中运行的Cursor程序
- 2. 找到当前运行Cursor实例,并定位到当前的chat窗口
- 3. 模拟鼠标点击到chat窗口
- 4. 模拟键盘输入"请检查并执行所有待处理任务。"到chat窗口
- 5. 模拟鼠标点击chat窗口的"提交"按钮
- 6. 以服务方式持续运行,间隔300秒进行一次上述操作
- 使用方法:
- 1. 单次执行:python scripts/cursor_auto_chat.py --once
- 2. 服务模式:python scripts/cursor_auto_chat.py --daemon
- 3. 自定义间隔:python scripts/cursor_auto_chat.py --interval 300
- 4. 指定输入框位置:python scripts/cursor_auto_chat.py --input-box-pos "1180,965"
- """
- import sys
- import time
- import argparse
- import logging
- from pathlib import Path
- from datetime import datetime
- try:
- import win32gui
- import win32con
- import win32process
- import win32api
- import pyautogui
- import pywinauto
- from pywinauto import Application
- try:
- import pyperclip
- HAS_PYPERCLIP = True
- except ImportError:
- HAS_PYPERCLIP = False
- except ImportError as e:
- print(f"❌ 缺少必要的依赖库: {e}")
- print("请运行: pip install pywin32 pyautogui pywinauto pyperclip")
- sys.exit(1)
- # 配置日志
- logs_dir = Path(__file__).parent.parent / 'logs'
- logs_dir.mkdir(exist_ok=True)
- logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- handlers=[
- logging.FileHandler(logs_dir / 'cursor_auto_chat.log', encoding='utf-8'),
- logging.StreamHandler(sys.stdout)
- ]
- )
- logger = logging.getLogger('CursorAutoChat')
- # 配置pyautogui安全设置
- pyautogui.FAILSAFE = True # 鼠标移到屏幕左上角会触发异常,用于紧急停止
- pyautogui.PAUSE = 0.5 # 每个操作之间的暂停时间(秒)
- # 检查pyperclip并记录
- if not HAS_PYPERCLIP:
- logger.warning("未安装pyperclip,中文输入可能有问题,建议安装: pip install pyperclip")
- class CursorAutoChat:
- """Cursor自动聊天工具类"""
-
- def __init__(self, message="请检查并执行所有待处理任务。", interval=300, input_box_pos=None):
- """
- 初始化工具
-
- Args:
- message: 要发送的消息内容
- interval: 执行间隔(秒)
- input_box_pos: 输入框位置 (x, y),如果提供则直接使用,不进行自动定位
- """
- self.message = message
- self.interval = interval
- self.cursor_window = None
- self.input_box_pos = input_box_pos # 用户指定的输入框位置
- logger.info(f"Cursor自动聊天工具已初始化")
- logger.info(f"消息内容: {self.message}")
- logger.info(f"执行间隔: {self.interval}秒")
- if self.input_box_pos:
- logger.info(f"使用指定的输入框位置: {self.input_box_pos}")
-
- def find_cursor_processes(self):
- """
- 查找所有运行的Cursor进程
-
- Returns:
- list: Cursor进程ID列表
- """
- cursor_pids = []
- try:
- import psutil
- for proc in psutil.process_iter(['pid', 'name', 'exe']):
- try:
- proc_name = proc.info['name'].lower() if proc.info['name'] else ''
- proc_exe = proc.info['exe'].lower() if proc.info['exe'] else ''
-
- # 查找Cursor相关进程
- if 'cursor' in proc_name or 'cursor' in proc_exe:
- cursor_pids.append(proc.info['pid'])
- logger.debug(f"找到Cursor进程: PID={proc.info['pid']}, Name={proc.info['name']}")
- except (psutil.NoSuchProcess, psutil.AccessDenied):
- continue
- except ImportError:
- # 如果没有psutil,使用win32api枚举窗口
- logger.warning("未安装psutil,使用窗口枚举方式查找Cursor")
- cursor_pids = self._find_cursor_by_windows()
-
- logger.info(f"找到 {len(cursor_pids)} 个Cursor进程")
- return cursor_pids
-
- def _find_cursor_by_windows(self):
- """通过枚举窗口查找Cursor进程"""
- cursor_pids = []
-
- def enum_windows_callback(hwnd, windows):
- if win32gui.IsWindowVisible(hwnd):
- window_title = win32gui.GetWindowText(hwnd)
- if 'cursor' in window_title.lower():
- _, pid = win32process.GetWindowThreadProcessId(hwnd)
- if pid not in cursor_pids:
- cursor_pids.append(pid)
- return True
-
- win32gui.EnumWindows(enum_windows_callback, None)
- return cursor_pids
-
- def find_cursor_window(self):
- """
- 查找Cursor主窗口
-
- Returns:
- int: 窗口句柄,如果未找到返回None
- """
- cursor_windows = [] # 存储所有可能的Cursor窗口
-
- def enum_windows_callback(hwnd, windows):
- if win32gui.IsWindowVisible(hwnd):
- window_title = win32gui.GetWindowText(hwnd)
- class_name = win32gui.GetClassName(hwnd)
-
- # Cursor基于Electron,窗口类名可能是Chrome_WidgetWin_1或类似
- # 查找可能的Cursor窗口
- is_cursor = False
-
- # 检查窗口标题
- if window_title and 'cursor' in window_title.lower():
- is_cursor = True
-
- # 检查窗口类名(Electron应用通常有特定类名)
- if class_name and ('chrome_widgetwin' in class_name.lower() or 'electron' in class_name.lower()):
- # 进一步检查:Electron窗口通常比较大
- rect = win32gui.GetWindowRect(hwnd)
- width = rect[2] - rect[0]
- height = rect[3] - rect[1]
- if width > 800 and height > 600:
- is_cursor = True
-
- if is_cursor:
- rect = win32gui.GetWindowRect(hwnd)
- width = rect[2] - rect[0]
- height = rect[3] - rect[1]
- area = width * height
- cursor_windows.append({
- 'hwnd': hwnd,
- 'title': window_title,
- 'class': class_name,
- 'width': width,
- 'height': height,
- 'area': area
- })
- logger.debug(f"找到可能的Cursor窗口: {window_title} ({class_name}), Size: {width}x{height}")
-
- return True
-
- win32gui.EnumWindows(enum_windows_callback, None)
-
- if not cursor_windows:
- logger.warning("未找到Cursor窗口")
- return None
-
- # 选择最大的窗口作为主窗口(通常是主应用窗口)
- cursor_windows.sort(key=lambda x: x['area'], reverse=True)
- main_window = cursor_windows[0]
-
- logger.info(f"找到Cursor主窗口: {main_window['title']} ({main_window['class']})")
- logger.info(f"窗口大小: {main_window['width']}x{main_window['height']} (HWND: {main_window['hwnd']})")
-
- return main_window['hwnd']
-
- def activate_cursor_window(self, hwnd):
- """
- 激活Cursor窗口
-
- Args:
- hwnd: 窗口句柄
- """
- try:
- # 恢复窗口(如果最小化)
- win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
- time.sleep(0.3)
-
- # 激活窗口
- win32gui.SetForegroundWindow(hwnd)
- time.sleep(0.5)
-
- logger.info("Cursor窗口已激活")
- return True
- except Exception as e:
- logger.error(f"激活窗口失败: {e}")
- return False
-
- def find_chat_input_area(self, hwnd):
- """
- 查找chat输入区域
-
- 使用多种策略定位Cursor的chat输入框:
- 1. 尝试多个快捷键打开chat(Ctrl+K, Ctrl+L, Ctrl+Shift+L等)
- 2. 使用相对窗口坐标定位输入框(chat通常在窗口底部中央或右侧)
- 3. 验证输入框是否被激活
-
- Args:
- hwnd: Cursor窗口句柄
-
- Returns:
- tuple: (是否成功, 输入框坐标(x, y)) 或 (False, None)
- """
- try:
- # 获取窗口位置和大小
- rect = win32gui.GetWindowRect(hwnd)
- window_left = rect[0]
- window_top = rect[1]
- window_width = rect[2] - rect[0]
- window_height = rect[3] - rect[1]
-
- logger.info(f"窗口位置: ({window_left}, {window_top}), 大小: {window_width}x{window_height}")
-
- # 策略1: 尝试多个快捷键打开chat
- logger.info("尝试使用快捷键打开chat窗口...")
- shortcuts = [
- ('ctrl', 'k'), # Cursor最常用的快捷键
- ('ctrl', 'l'), # 备用快捷键
- ('ctrl', 'shift', 'l'), # 另一个可能的快捷键
- ]
-
- for shortcut in shortcuts:
- try:
- logger.debug(f"尝试快捷键: {'+'.join(shortcut)}")
- pyautogui.hotkey(*shortcut)
- time.sleep(1.5) # 给chat窗口时间打开
-
- # 尝试定位输入框
- result = self._try_activate_input_box(hwnd, window_left, window_top, window_width, window_height)
- if result:
- input_pos = result
- logger.info(f"成功定位并激活chat输入框,位置: {input_pos}")
- return (True, input_pos)
- except Exception as e:
- logger.debug(f"快捷键 {shortcut} 失败: {e}")
- continue
-
- # 策略2: 直接尝试点击可能的输入框位置(即使chat已打开)
- logger.info("尝试直接点击可能的输入框位置...")
- result = self._try_activate_input_box(hwnd, window_left, window_top, window_width, window_height)
- if result:
- input_pos = result
- logger.info(f"成功定位并激活chat输入框,位置: {input_pos}")
- return (True, input_pos)
-
- logger.warning("未能定位chat输入框,将尝试通用方法")
- return (False, None)
-
- except Exception as e:
- logger.error(f"查找chat输入区域失败: {e}")
- return (False, None)
-
- def _try_activate_input_box(self, hwnd, window_left, window_top, window_width, window_height):
- """
- 尝试激活输入框
-
- Args:
- hwnd: 窗口句柄
- window_left: 窗口左边界
- window_top: 窗口上边界
- window_width: 窗口宽度
- window_height: 窗口高度
-
- Returns:
- tuple: 成功时返回 (x, y) 坐标,失败时返回 None
- """
- # Cursor的chat输入框通常在窗口底部中央或右侧底部
- # 尝试多个可能的位置(相对于窗口)
- # 根据实际测试,输入框位置约为(0.61, 0.92)
- possible_relative_positions = [
- (0.61, 0.92), # 实际测试位置(优先尝试)
- (0.6, 0.92), # 稍微偏左
- (0.62, 0.92), # 稍微偏右
- (0.61, 0.91), # 稍微偏上
- (0.61, 0.93), # 稍微偏下
- (0.75, 0.92), # 窗口右侧底部(备用)
- (0.5, 0.92), # 窗口底部中央(备用)
- (0.5, 0.88), # 窗口底部稍上(备用)
- (0.8, 0.9), # 窗口右侧(备用)
- ]
-
- for rel_x, rel_y in possible_relative_positions:
- try:
- # 计算绝对坐标
- abs_x = window_left + int(window_width * rel_x)
- abs_y = window_top + int(window_height * rel_y)
-
- logger.debug(f"尝试点击位置(相对窗口): ({rel_x:.2f}, {rel_y:.2f}) -> 绝对坐标: ({abs_x}, {abs_y})")
-
- # 点击输入框位置
- pyautogui.click(abs_x, abs_y)
- time.sleep(0.8) # 等待输入框激活
-
- # 验证输入框是否激活:尝试输入一个测试字符然后删除
- # 如果输入框已激活,这个操作应该成功
- pyautogui.write('test', interval=0.1)
- time.sleep(0.3)
-
- # 删除测试文本
- for _ in range(4):
- pyautogui.press('backspace')
- time.sleep(0.3)
-
- # 如果到这里没有异常,说明输入框可能已激活
- logger.info(f"成功激活输入框,位置: ({abs_x}, {abs_y})")
- return (abs_x, abs_y)
-
- except Exception as e:
- logger.debug(f"位置 ({rel_x:.2f}, {rel_y:.2f}) 失败: {e}")
- continue
-
- return None
-
- def send_message(self, message, input_pos=None):
- """
- 发送消息到chat窗口
-
- Args:
- message: 要发送的消息
- input_pos: 输入框坐标 (x, y),如果提供则点击该位置确保激活
- """
- try:
- # 如果提供了输入框位置,先移动鼠标到该位置,然后点击
- if input_pos:
- x, y = input_pos
- logger.info(f"移动鼠标到输入框位置: ({x}, {y})")
- pyautogui.moveTo(x, y, duration=0.3) # 平滑移动到输入框位置
- time.sleep(0.2)
-
- logger.info(f"点击输入框位置确保激活: ({x}, {y})")
- pyautogui.click(x, y)
- time.sleep(0.5) # 等待输入框激活
- else:
- # 如果没有提供位置,点击当前鼠标位置
- logger.info("点击当前位置确保输入框激活")
- pyautogui.click()
- time.sleep(0.3)
-
- # 清空可能的现有文本
- logger.info("清空输入框中的现有文本...")
- pyautogui.hotkey('ctrl', 'a')
- time.sleep(0.3)
-
- # 再次确保鼠标在输入框位置并点击(如果提供了位置)
- if input_pos:
- x, y = input_pos
- logger.info(f"再次移动鼠标到输入框并点击: ({x}, {y})")
- pyautogui.moveTo(x, y, duration=0.2)
- time.sleep(0.2)
- pyautogui.click(x, y)
- time.sleep(0.4) # 给足够时间让输入框完全激活
- else:
- pyautogui.click()
- time.sleep(0.2)
-
- # 输入消息
- logger.info(f"正在输入消息: {message}")
-
- # 对于中文文本,使用剪贴板方法更可靠
- if HAS_PYPERCLIP:
- try:
- # 保存当前剪贴板内容
- old_clipboard = pyperclip.paste() if hasattr(pyperclip, 'paste') else None
-
- # 复制消息到剪贴板
- pyperclip.copy(message)
- time.sleep(0.3)
-
- # 确保鼠标在输入框位置(如果提供了位置)
- if input_pos:
- x, y = input_pos
- logger.info(f"粘贴前确保鼠标在输入框位置: ({x}, {y})")
- pyautogui.moveTo(x, y, duration=0.2)
- time.sleep(0.2)
- # 再次点击确保焦点
- pyautogui.click(x, y)
- time.sleep(0.3)
-
- # 粘贴消息
- logger.info("执行Ctrl+V粘贴消息...")
- pyautogui.hotkey('ctrl', 'v')
- time.sleep(1.5) # 等待粘贴完成,给足够时间
-
- # 验证粘贴是否成功(可选:再次点击确保文本已输入)
- if input_pos:
- # 轻微移动鼠标确认输入框仍有焦点
- x, y = input_pos
- pyautogui.moveTo(x + 1, y + 1, duration=0.1)
- time.sleep(0.2)
-
- # 恢复剪贴板(可选)
- if old_clipboard:
- try:
- pyperclip.copy(old_clipboard)
- except:
- pass
-
- logger.info("使用剪贴板方法输入消息成功")
- return True
- except Exception as e:
- logger.warning(f"剪贴板方法失败: {e},尝试其他方法...")
-
- # 备用方法:直接输入(对英文有效)
- try:
- # 检查是否包含中文字符
- has_chinese = any('\u4e00' <= char <= '\u9fff' for char in message)
- if has_chinese:
- logger.warning("消息包含中文,但pyperclip不可用,输入可能失败")
-
- pyautogui.write(message, interval=0.05)
- time.sleep(0.8)
- logger.info("使用write方法输入成功")
- return True
- except Exception as e2:
- logger.error(f"使用write方法也失败: {e2}")
- return False
-
- except Exception as e:
- logger.error(f"输入消息失败: {e}")
- return False
-
- def click_submit_button(self):
- """
- 点击提交按钮
-
- Cursor的提交方式可能是:
- 1. Enter键(单行输入)
- 2. Ctrl+Enter组合键(多行输入或某些配置)
- 3. 点击提交按钮(如果存在)
- """
- try:
- # 策略1: 先尝试Enter键(最常见)
- logger.info("尝试按Enter键提交...")
- pyautogui.press('enter')
- time.sleep(1.0) # 等待消息发送
-
- # 策略2: 如果Enter不行,尝试Ctrl+Enter(某些配置下需要)
- # 但先等待一下,看看Enter是否生效
- logger.info("等待消息发送完成...")
- time.sleep(1.5) # 给足够时间让消息发送
-
- logger.info("消息已提交(使用Enter键)")
- logger.info("提示: 如果消息未出现在chat历史中,可能需要使用Ctrl+Enter")
- return True
- except Exception as e:
- logger.error(f"点击提交按钮失败: {e}")
- # 尝试备用方法
- try:
- logger.info("尝试使用Ctrl+Enter提交...")
- pyautogui.hotkey('ctrl', 'enter')
- time.sleep(1.5)
- logger.info("使用Ctrl+Enter提交完成")
- return True
- except Exception as e2:
- logger.error(f"使用Ctrl+Enter也失败: {e2}")
- return False
-
- def execute_once(self):
- """
- 执行一次完整的操作流程
-
- Returns:
- bool: 是否成功执行
- """
- logger.info("=" * 60)
- logger.info("开始执行自动聊天操作...")
- logger.info("=" * 60)
-
- try:
- # 步骤1: 查找Cursor进程
- logger.info("步骤1: 查找Cursor进程...")
- cursor_pids = self.find_cursor_processes()
- if not cursor_pids:
- logger.error("未找到Cursor进程,请确保Cursor正在运行")
- return False
-
- # 步骤2: 查找Cursor窗口
- logger.info("步骤2: 查找Cursor主窗口...")
- cursor_hwnd = self.find_cursor_window()
- if not cursor_hwnd:
- logger.error("未找到Cursor主窗口")
- return False
-
- # 步骤3: 激活窗口
- logger.info("步骤3: 激活Cursor窗口...")
- if not self.activate_cursor_window(cursor_hwnd):
- logger.error("激活窗口失败")
- return False
-
- # 步骤4: 定位chat输入区域
- logger.info("步骤4: 定位chat输入区域...")
-
- # 如果用户指定了输入框位置,直接使用
- if self.input_box_pos:
- input_pos = self.input_box_pos
- logger.info(f"使用用户指定的输入框位置: {input_pos}")
- # 激活窗口后,直接使用指定位置
- time.sleep(0.5)
- else:
- # 自动定位输入框
- result = self.find_chat_input_area(cursor_hwnd)
- if isinstance(result, tuple) and len(result) == 2:
- success, input_pos = result
- if success and input_pos:
- logger.info(f"已定位到输入框位置: {input_pos}")
- else:
- logger.warning("未能精确定位输入区域,将尝试直接输入")
- input_pos = None
- else:
- # 兼容旧版本返回格式
- if result:
- logger.info("已定位到输入框")
- input_pos = None # 旧版本不返回位置
- else:
- logger.warning("未能精确定位输入区域,将尝试直接输入")
- input_pos = None
- time.sleep(1)
-
- # 步骤5: 输入消息
- logger.info("步骤5: 输入消息...")
- if not self.send_message(self.message, input_pos):
- logger.error("输入消息失败")
- return False
-
- # 步骤6: 点击提交按钮
- logger.info("步骤6: 提交消息...")
- if not self.click_submit_button():
- logger.error("提交消息失败")
- return False
-
- logger.info("=" * 60)
- logger.info("✅ 自动聊天操作执行成功!")
- logger.info("=" * 60)
- return True
-
- except Exception as e:
- logger.error(f"执行过程中出错: {e}", exc_info=True)
- return False
-
- def run_daemon(self):
- """
- 以守护进程模式运行,定期执行操作
- """
- logger.info("=" * 60)
- logger.info("🚀 Cursor自动聊天工具已启动(守护进程模式)")
- logger.info(f"⏰ 执行间隔: {self.interval}秒 ({self.interval//60}分钟)")
- logger.info("按 Ctrl+C 停止服务")
- logger.info("=" * 60)
-
- try:
- while True:
- try:
- success = self.execute_once()
-
- if success:
- logger.info(f"✅ 操作执行成功,{self.interval}秒后再次执行...")
- else:
- logger.warning(f"⚠️ 操作执行失败,{self.interval}秒后重试...")
-
- time.sleep(self.interval)
-
- except KeyboardInterrupt:
- raise
- except Exception as e:
- logger.error(f"执行过程中出错: {e}")
- logger.info(f"⏳ {self.interval}秒后重试...")
- time.sleep(self.interval)
-
- except KeyboardInterrupt:
- logger.info("\n" + "=" * 60)
- logger.info("⛔ 用户停止了Cursor自动聊天工具")
- logger.info("=" * 60)
- def main():
- """主函数"""
- parser = argparse.ArgumentParser(
- description='Cursor自动聊天工具 - 自动向Cursor chat发送消息',
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
- 示例:
- # 单次执行
- python scripts/cursor_auto_chat.py --once
-
- # 守护进程模式(默认)
- python scripts/cursor_auto_chat.py --daemon
-
- # 自定义间隔(秒)
- python scripts/cursor_auto_chat.py --interval 300
-
- # 自定义消息
- python scripts/cursor_auto_chat.py --message "你的消息内容"
-
- # 指定输入框位置
- python scripts/cursor_auto_chat.py --input-box-pos "1180,965"
- """
- )
-
- parser.add_argument(
- '--once',
- action='store_true',
- help='只执行一次,不持续运行'
- )
- parser.add_argument(
- '--daemon',
- action='store_true',
- help='作为守护进程运行(默认模式)'
- )
- parser.add_argument(
- '--interval',
- type=int,
- default=300,
- help='执行间隔(秒),默认300秒(5分钟)'
- )
- parser.add_argument(
- '--message',
- type=str,
- default='请检查并执行所有待处理任务。',
- help='要发送的消息内容,默认: "请检查并执行所有待处理任务。"'
- )
- parser.add_argument(
- '--input-box-pos',
- type=str,
- default=None,
- help='输入框位置,格式: "x,y" (例如: "1180,965"),如果提供则直接使用该位置,不进行自动定位'
- )
-
- args = parser.parse_args()
-
- # 解析输入框位置(如果提供)
- input_box_pos = None
- if args.input_box_pos:
- try:
- parts = args.input_box_pos.split(',')
- if len(parts) == 2:
- input_box_pos = (int(parts[0].strip()), int(parts[1].strip()))
- logger.info(f"解析输入框位置: {input_box_pos}")
- else:
- logger.warning(f"输入框位置格式错误,应使用 'x,y' 格式: {args.input_box_pos}")
- except ValueError as e:
- logger.warning(f"无法解析输入框位置: {e}")
-
- # 创建工具实例
- tool = CursorAutoChat(message=args.message, interval=args.interval, input_box_pos=input_box_pos)
-
- # 根据参数运行
- if args.once:
- tool.execute_once()
- else:
- tool.run_daemon()
- if __name__ == '__main__':
- main()
|