auto_execute_tasks.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. #!/usr/bin/env python3
  2. """
  3. 自动任务执行 + Cursor Chat 工具(合并版)
  4. 本脚本整合了原来的:
  5. - auto_execute_tasks.py:检查数据库中的 pending 任务,生成本地任务文件和 pending_tasks.json
  6. - cursor_auto_chat.py:在 Windows 下自动操作 Cursor Chat,发送指定消息
  7. 合并后,一个脚本即可完成:
  8. 1. 从 task_list 中读取 pending 任务
  9. 2. 为任务生成本地 Python 占位文件
  10. 3. 维护 .cursor/pending_tasks.json
  11. 4. (可选)在 Cursor Chat 中自动发起「请检查并执行所有待处理任务。」
  12. 5. 将 .cursor/pending_tasks.json 中 status=completed 的任务状态同步到 task_list
  13. 使用方式:
  14. 1. 仅任务调度(不发 Chat):
  15. python scripts/auto_execute_tasks.py --once
  16. python scripts/auto_execute_tasks.py --interval 300
  17. 2. 任务调度 + 自动 Chat:
  18. python scripts/auto_execute_tasks.py --once --enable-chat --chat-input-pos "1180,965"
  19. python scripts/auto_execute_tasks.py --interval 300 --enable-chat --chat-input-pos "1180,965"
  20. """
  21. from __future__ import annotations
  22. import json
  23. import time
  24. import argparse
  25. import logging
  26. import sys
  27. from pathlib import Path
  28. from datetime import datetime
  29. from typing import Any, Dict, List, Optional, Tuple
  30. # 配置日志
  31. logging.basicConfig(
  32. level=logging.INFO,
  33. format="%(asctime)s - %(levelname)s - %(message)s",
  34. )
  35. logger = logging.getLogger("AutoExecuteTasks")
  36. # ==== Cursor Chat 相关依赖(Windows GUI 自动化) ====
  37. try:
  38. import win32gui
  39. import win32con
  40. import win32process
  41. import pyautogui
  42. try:
  43. import pyperclip
  44. HAS_PYPERCLIP = True
  45. except ImportError:
  46. HAS_PYPERCLIP = False
  47. HAS_CURSOR_GUI = True
  48. # pyautogui 安全 & 节奏
  49. pyautogui.FAILSAFE = True
  50. pyautogui.PAUSE = 0.5
  51. except ImportError:
  52. HAS_CURSOR_GUI = False
  53. HAS_PYPERCLIP = False
  54. logger.warning(
  55. "未安装 Windows GUI 自动化依赖(pywin32/pyautogui/pyperclip),"
  56. "将禁用自动 Cursor Chat 功能。"
  57. )
  58. # 全局配置(由命令行参数控制)
  59. ENABLE_CHAT: bool = False
  60. CHAT_MESSAGE: str = "请检查并执行所有待处理任务。"
  61. CHAT_INPUT_POS: Optional[Tuple[int, int]] = None
  62. WORKSPACE_ROOT = Path(__file__).parent.parent
  63. CURSOR_DIR = WORKSPACE_ROOT / ".cursor"
  64. PENDING_TASKS_FILE = CURSOR_DIR / "pending_tasks.json"
  65. def get_db_connection():
  66. """
  67. 获取数据库连接
  68. Returns:
  69. 数据库连接对象,如果失败返回None
  70. """
  71. try:
  72. import psycopg2
  73. from psycopg2.extras import RealDictCursor
  74. # 读取数据库配置
  75. config_file = Path(__file__).parent.parent / 'mcp-servers' / 'task-manager' / 'config.json'
  76. with open(config_file, 'r', encoding='utf-8') as f:
  77. config = json.load(f)
  78. db_uri = config['database']['uri']
  79. conn = psycopg2.connect(db_uri)
  80. return conn
  81. except Exception as e:
  82. logger.error(f"连接数据库失败: {e}")
  83. return None
  84. def get_pending_tasks():
  85. """
  86. 从数据库获取所有pending任务
  87. """
  88. try:
  89. from psycopg2.extras import RealDictCursor
  90. conn = get_db_connection()
  91. if not conn:
  92. return []
  93. cursor = conn.cursor(cursor_factory=RealDictCursor)
  94. # 查询pending任务
  95. cursor.execute("""
  96. SELECT task_id, task_name, task_description, status,
  97. code_name, code_path, create_time, create_by
  98. FROM task_list
  99. WHERE status = 'pending'
  100. ORDER BY create_time ASC
  101. """)
  102. tasks = cursor.fetchall()
  103. cursor.close()
  104. conn.close()
  105. return [dict(task) for task in tasks]
  106. except Exception as e:
  107. logger.error(f"获取pending任务失败: {e}")
  108. return []
  109. def update_task_status(task_id, status, code_name=None, code_path=None):
  110. """
  111. 更新任务状态
  112. Args:
  113. task_id: 任务ID
  114. status: 新状态('pending', 'processing', 'completed', 'failed')
  115. code_name: 代码文件名(可选)
  116. code_path: 代码文件路径(可选)
  117. Returns:
  118. 是否更新成功
  119. """
  120. try:
  121. conn = get_db_connection()
  122. if not conn:
  123. return False
  124. cursor = conn.cursor()
  125. # 构建更新SQL
  126. if code_name and code_path:
  127. cursor.execute("""
  128. UPDATE task_list
  129. SET status = %s, code_name = %s, code_path = %s,
  130. update_time = CURRENT_TIMESTAMP
  131. WHERE task_id = %s
  132. """, (status, code_name, code_path, task_id))
  133. else:
  134. cursor.execute("""
  135. UPDATE task_list
  136. SET status = %s, update_time = CURRENT_TIMESTAMP
  137. WHERE task_id = %s
  138. """, (status, task_id))
  139. conn.commit()
  140. updated = cursor.rowcount > 0
  141. cursor.close()
  142. conn.close()
  143. if updated:
  144. logger.info(f"✅ 任务 {task_id} 状态已更新为: {status}")
  145. else:
  146. logger.warning(f"⚠️ 任务 {task_id} 状态更新失败(任务不存在)")
  147. return updated
  148. except Exception as e:
  149. logger.error(f"更新任务状态失败: {e}")
  150. return False
  151. # ==== Cursor Chat 辅助函数(简化版,依赖固定输入框坐标) ====
  152. def _find_cursor_window() -> Optional[int]:
  153. """
  154. 查找 Cursor 主窗口句柄(简化版)
  155. 通过窗口标题 / 类名中包含 'Cursor' / 'Chrome_WidgetWin_1' 来判断。
  156. """
  157. if not HAS_CURSOR_GUI:
  158. return None
  159. cursor_windows: List[Dict[str, Any]] = []
  160. def enum_windows_callback(hwnd, _extra):
  161. if win32gui.IsWindowVisible(hwnd):
  162. title = win32gui.GetWindowText(hwnd) or ""
  163. class_name = win32gui.GetClassName(hwnd) or ""
  164. is_cursor = False
  165. if "cursor" in title.lower():
  166. is_cursor = True
  167. if class_name and "chrome_widgetwin" in class_name.lower():
  168. is_cursor = True
  169. if is_cursor:
  170. left, top, right, bottom = win32gui.GetWindowRect(hwnd)
  171. width = right - left
  172. height = bottom - top
  173. area = width * height
  174. cursor_windows.append(
  175. {
  176. "hwnd": hwnd,
  177. "title": title,
  178. "class": class_name,
  179. "width": width,
  180. "height": height,
  181. "area": area,
  182. }
  183. )
  184. return True
  185. win32gui.EnumWindows(enum_windows_callback, None)
  186. if not cursor_windows:
  187. logger.warning("未找到 Cursor 窗口")
  188. return None
  189. # 选取面积最大的窗口作为主窗口
  190. cursor_windows.sort(key=lambda x: x["area"], reverse=True)
  191. main = cursor_windows[0]
  192. logger.info(
  193. "找到 Cursor 主窗口: %s (%s), size=%dx%d, hwnd=%s",
  194. main["title"],
  195. main["class"],
  196. main["width"],
  197. main["height"],
  198. main["hwnd"],
  199. )
  200. return main["hwnd"]
  201. def _activate_cursor_window(hwnd: int) -> bool:
  202. """激活 Cursor 主窗口"""
  203. if not HAS_CURSOR_GUI:
  204. return False
  205. try:
  206. win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
  207. time.sleep(0.3)
  208. win32gui.SetForegroundWindow(hwnd)
  209. time.sleep(0.5)
  210. logger.info("Cursor 窗口已激活")
  211. return True
  212. except Exception as exc: # noqa: BLE001
  213. logger.error("激活 Cursor 窗口失败: %s", exc)
  214. return False
  215. def _send_chat_message_once(message: str, input_pos: Optional[Tuple[int, int]]) -> bool:
  216. """
  217. 在 Cursor Chat 中发送一条消息(单次):
  218. 1. 激活窗口
  219. 2. 如果提供了输入框坐标,则移动并点击
  220. 3. 使用剪贴板粘贴消息
  221. 4. 按 Enter 提交
  222. """
  223. if not HAS_CURSOR_GUI:
  224. logger.warning("当前环境不支持 Cursor GUI 自动化,跳过自动发 Chat")
  225. return False
  226. hwnd = _find_cursor_window()
  227. if not hwnd:
  228. return False
  229. if not _activate_cursor_window(hwnd):
  230. return False
  231. # 点击/激活输入框
  232. if input_pos:
  233. x, y = input_pos
  234. logger.info("移动鼠标到输入框位置: (%d, %d)", x, y)
  235. pyautogui.moveTo(x, y, duration=0.3)
  236. time.sleep(0.2)
  237. pyautogui.click(x, y)
  238. time.sleep(0.4)
  239. else:
  240. # 未指定位置时,尝试快捷键打开 Chat(Ctrl+K),然后点击窗口底部中间
  241. logger.info("未指定输入框位置,尝试使用 Ctrl+K 打开 Chat")
  242. pyautogui.hotkey("ctrl", "k")
  243. time.sleep(1.5)
  244. screen_width, screen_height = pyautogui.size()
  245. x, y = int(screen_width * 0.6), int(screen_height * 0.9)
  246. pyautogui.moveTo(x, y, duration=0.3)
  247. pyautogui.click(x, y)
  248. time.sleep(0.4)
  249. # 清空旧内容
  250. pyautogui.hotkey("ctrl", "a")
  251. time.sleep(0.3)
  252. # 再次点击保证焦点
  253. pyautogui.click()
  254. time.sleep(0.2)
  255. logger.info("正在向 Cursor Chat 输入消息: %s", message)
  256. # 优先使用剪贴板(兼容中文)
  257. if HAS_PYPERCLIP:
  258. try:
  259. old_clipboard = pyperclip.paste()
  260. except Exception: # noqa: BLE001
  261. old_clipboard = None
  262. try:
  263. pyperclip.copy(message)
  264. time.sleep(0.3)
  265. pyautogui.hotkey("ctrl", "v")
  266. time.sleep(1.0)
  267. logger.info("使用剪贴板粘贴消息成功")
  268. except Exception as exc: # noqa: BLE001
  269. logger.error("剪贴板粘贴失败: %s,退回到直接输入", exc)
  270. pyautogui.write(message, interval=0.05)
  271. time.sleep(0.8)
  272. finally:
  273. if old_clipboard is not None:
  274. try:
  275. pyperclip.copy(old_clipboard)
  276. except Exception:
  277. pass
  278. else:
  279. pyautogui.write(message, interval=0.05)
  280. time.sleep(0.8)
  281. # 提交(Enter)
  282. logger.info("按 Enter 提交消息")
  283. pyautogui.press("enter")
  284. time.sleep(1.0)
  285. logger.info("消息已提交")
  286. return True
  287. def send_chat_for_pending_tasks() -> None:
  288. """
  289. 如果启用了 Chat 功能且存在 pending 任务,则向 Cursor Chat 发送一次统一消息。
  290. """
  291. if not ENABLE_CHAT:
  292. return
  293. # 仅检查 .cursor/pending_tasks.json 中是否存在 status 为 processing 的任务
  294. if not PENDING_TASKS_FILE.exists():
  295. logger.info("未找到 .cursor/pending_tasks.json,跳过自动 Chat")
  296. return
  297. try:
  298. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  299. data = json.load(f)
  300. if not isinstance(data, list) or not any(
  301. t.get("status") == "processing" for t in data
  302. ):
  303. logger.info("pending_tasks.json 中没有 processing 任务,跳过自动 Chat")
  304. return
  305. except Exception as exc: # noqa: BLE001
  306. logger.error("读取 pending_tasks.json 失败: %s", exc)
  307. return
  308. logger.info("检测到 processing 任务,准备自动向 Cursor Chat 发送提醒消息")
  309. _send_chat_message_once(CHAT_MESSAGE, CHAT_INPUT_POS)
  310. def sync_completed_tasks_from_pending_file() -> int:
  311. """
  312. 将 .cursor/pending_tasks.json 中 status == 'completed' 的任务,同步到数据库。
  313. Returns:
  314. 成功更新的任务数量
  315. """
  316. if not PENDING_TASKS_FILE.exists():
  317. return 0
  318. try:
  319. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  320. tasks = json.load(f)
  321. except Exception as exc: # noqa: BLE001
  322. logger.error("读取 pending_tasks.json 失败: %s", exc)
  323. return 0
  324. if not isinstance(tasks, list):
  325. return 0
  326. updated = 0
  327. for t in tasks:
  328. if t.get("status") != "completed":
  329. continue
  330. task_id = t.get("task_id")
  331. if not task_id:
  332. continue
  333. code_name = t.get("code_name")
  334. code_path = t.get("code_path")
  335. if update_task_status(task_id, "completed", code_name, code_path):
  336. updated += 1
  337. logger.info(
  338. "已根据 pending_tasks.json 将任务 %s 同步为 completed (code_name=%s, code_path=%s)",
  339. task_id,
  340. code_name,
  341. code_path,
  342. )
  343. if updated:
  344. logger.info("本次共同步 %d 个 completed 任务到数据库", updated)
  345. return updated
  346. def print_task_for_cursor_execution(task, task_file_path=None):
  347. """
  348. 打印任务信息,供Cursor识别并执行
  349. 这个函数会以特定格式输出任务,Cursor可以识别并自动执行
  350. """
  351. print("\n" + "=" * 80)
  352. print(f"🤖 [AUTO-EXECUTE-TASK] Task ID: {task['task_id']}")
  353. print("=" * 80)
  354. print(f"\n**任务名称**: {task['task_name']}")
  355. print(f"**任务ID**: {task['task_id']}")
  356. print(f"**状态**: processing(已更新)")
  357. print(f"**创建时间**: {task['create_time']}")
  358. print(f"**创建者**: {task['create_by']}")
  359. if task_file_path:
  360. print(f"**任务文件**: {task_file_path}")
  361. print(f"\n## 任务描述\n")
  362. print(task['task_description'])
  363. print(f"\n## 执行指令")
  364. print(f"\n请Cursor AI根据上述任务描述,自动完成以下步骤:")
  365. print(f"1. 打开并查看任务文件: {task_file_path or '未创建'}")
  366. print(f"2. 根据任务描述实现具体功能")
  367. print(f"3. 确保代码符合项目规范")
  368. print(f"4. 完成后调用MCP工具更新任务状态:")
  369. print(f" 工具: update_task_status")
  370. print(f" 参数: {{")
  371. print(f" \"task_id\": {task['task_id']},")
  372. if task_file_path:
  373. import os
  374. file_name = os.path.basename(task_file_path)
  375. file_dir = os.path.dirname(task_file_path).replace(str(Path(__file__).parent.parent), '').strip('\\').strip('/')
  376. print(f" \"code_name\": \"{file_name}\",")
  377. print(f" \"code_path\": \"{file_dir}\",")
  378. print(f" \"status\": \"completed\"")
  379. print(f" }}")
  380. print(f"\n任务文件保存路径:{task.get('code_path', 'app/core/data_flow')}")
  381. print("\n" + "=" * 80)
  382. print(f"🔚 [END-AUTO-EXECUTE-TASK]")
  383. print("=" * 80 + "\n")
  384. def create_task_file(task):
  385. """
  386. 在指定目录创建任务文件
  387. Args:
  388. task: 任务字典
  389. Returns:
  390. 生成的文件路径,如果失败返回None
  391. """
  392. try:
  393. workspace = Path(__file__).parent.parent
  394. code_path = task.get('code_path', 'app/core/data_flow')
  395. target_dir = workspace / code_path
  396. # 确保目录存在
  397. target_dir.mkdir(parents=True, exist_ok=True)
  398. # 生成文件名(从任务名称或代码名称)
  399. code_name = task.get('code_name')
  400. if not code_name:
  401. # 从任务名称生成文件名
  402. import re
  403. task_name = task['task_name']
  404. # 清理文件名:去除特殊字符,替换为下划线
  405. safe_name = re.sub(r'[^\w\u4e00-\u9fff]+', '_', task_name)
  406. safe_name = re.sub(r'_+', '_', safe_name).strip('_')
  407. code_name = f"{safe_name}.py"
  408. # 确保是.py文件
  409. if not code_name.endswith('.py'):
  410. code_name = f"{code_name}.py"
  411. file_path = target_dir / code_name
  412. # 如果文件已存在,添加时间戳
  413. if file_path.exists():
  414. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  415. base_name = file_path.stem
  416. file_path = target_dir / f"{base_name}_{timestamp}.py"
  417. code_name = file_path.name
  418. # 生成任务文件内容
  419. file_content = f'''#!/usr/bin/env python3
  420. """
  421. {task['task_name']}
  422. 任务ID: {task['task_id']}
  423. 创建时间: {task['create_time']}
  424. 创建者: {task['create_by']}
  425. 任务描述:
  426. {task['task_description']}
  427. 注意:此文件为任务占位符,需要根据任务描述实现具体功能。
  428. """
  429. # TODO: 根据任务描述实现功能
  430. # {task['task_description'][:100]}...
  431. if __name__ == '__main__':
  432. print("任务文件已创建,请根据任务描述实现具体功能")
  433. pass
  434. '''
  435. # 写入文件
  436. with open(file_path, 'w', encoding='utf-8') as f:
  437. f.write(file_content)
  438. logger.info(f"✅ 任务文件已创建: {file_path}")
  439. # 更新数据库中的code_name和code_path
  440. update_task_status(
  441. task['task_id'],
  442. 'processing', # 状态改为processing
  443. code_name=code_name,
  444. code_path=code_path
  445. )
  446. return str(file_path)
  447. except Exception as e:
  448. logger.error(f"创建任务文件失败: {e}")
  449. # 即使文件创建失败,也要更新状态为processing
  450. update_task_status(task['task_id'], 'processing')
  451. return None
  452. def notify_cursor_to_execute_task(task, task_file_path=None):
  453. """
  454. 通知Cursor执行任务
  455. 通过创建一个标记文件,让Cursor知道有新任务需要执行
  456. """
  457. workspace = Path(__file__).parent.parent
  458. task_trigger_file = workspace / '.cursor' / 'pending_tasks.json'
  459. task_trigger_file.parent.mkdir(parents=True, exist_ok=True)
  460. # 读取现有的pending tasks
  461. pending_tasks = []
  462. if task_trigger_file.exists():
  463. try:
  464. with open(task_trigger_file, 'r', encoding='utf-8') as f:
  465. pending_tasks = json.load(f)
  466. except:
  467. pending_tasks = []
  468. # 检查任务是否已存在
  469. task_exists = any(t['task_id'] == task['task_id'] for t in pending_tasks)
  470. if not task_exists:
  471. task_info = {
  472. 'task_id': task['task_id'],
  473. 'task_name': task['task_name'],
  474. 'task_description': task['task_description'],
  475. 'code_path': task.get('code_path', 'app/core/data_flow'),
  476. 'code_name': task.get('code_name'),
  477. 'status': 'processing', # 标记为processing
  478. 'notified_at': datetime.now().isoformat()
  479. }
  480. if task_file_path:
  481. task_info['task_file'] = task_file_path
  482. pending_tasks.append(task_info)
  483. # 写入文件
  484. with open(task_trigger_file, 'w', encoding='utf-8') as f:
  485. json.dump(pending_tasks, f, indent=2, ensure_ascii=False)
  486. logger.info(f"✅ Task {task['task_id']} added to pending_tasks.json")
  487. def auto_execute_tasks_once():
  488. """
  489. 执行一次任务检查和通知
  490. """
  491. # 先尝试把本地 completed 状态同步到数据库
  492. sync_completed_tasks_from_pending_file()
  493. logger.info("🔍 检查pending任务...")
  494. tasks = get_pending_tasks()
  495. if not tasks:
  496. logger.info("✅ 没有pending任务")
  497. return 0
  498. logger.info(f"📋 找到 {len(tasks)} 个pending任务")
  499. processed_count = 0
  500. for task in tasks:
  501. logger.info(f"\n{'='*80}")
  502. logger.info(f"处理任务: [{task['task_id']}] {task['task_name']}")
  503. logger.info(f"{'='*80}")
  504. try:
  505. # 1. 创建任务文件(同时更新状态为processing)
  506. task_file_path = create_task_file(task)
  507. if not task_file_path:
  508. logger.warning(f"⚠️ 任务 {task['task_id']} 文件创建失败,但状态已更新为processing")
  509. # 即使文件创建失败,也继续通知Cursor
  510. # 2. 打印任务详情,供Cursor识别
  511. print_task_for_cursor_execution(task, task_file_path)
  512. # 3. 创建通知文件
  513. notify_cursor_to_execute_task(task, task_file_path)
  514. processed_count += 1
  515. logger.info(f"✅ 任务 {task['task_id']} 处理完成")
  516. except Exception as e:
  517. logger.error(f"❌ 处理任务 {task['task_id']} 时出错: {e}")
  518. # 标记任务为failed
  519. update_task_status(task['task_id'], 'failed')
  520. return processed_count
  521. def auto_execute_tasks_loop(interval=300):
  522. """
  523. 循环执行任务检查
  524. Args:
  525. interval: 检查间隔(秒),默认300秒(5分钟)
  526. """
  527. logger.info("=" * 80)
  528. logger.info("🚀 自动任务执行服务已启动")
  529. logger.info(f"⏰ 检查间隔: {interval}秒 ({interval//60}分钟)")
  530. logger.info("按 Ctrl+C 停止服务")
  531. logger.info("=" * 80 + "\n")
  532. try:
  533. while True:
  534. try:
  535. count = auto_execute_tasks_once()
  536. # 如果有新任务且启用了自动 Chat,则向 Cursor 发送提醒
  537. if count > 0:
  538. send_chat_for_pending_tasks()
  539. if count > 0:
  540. logger.info(f"\n✅ 已通知 {count} 个任务")
  541. logger.info(f"\n⏳ 下次检查时间: {interval}秒后...")
  542. time.sleep(interval)
  543. except KeyboardInterrupt:
  544. raise
  545. except Exception as e:
  546. logger.error(f"❌ 执行出错: {e}")
  547. logger.info(f"⏳ {interval}秒后重试...")
  548. time.sleep(interval)
  549. except KeyboardInterrupt:
  550. logger.info("\n" + "=" * 80)
  551. logger.info("⛔ 用户停止了自动任务执行服务")
  552. logger.info("=" * 80)
  553. def main():
  554. """
  555. 主函数
  556. """
  557. parser = argparse.ArgumentParser(
  558. description='自动任务执行脚本(含可选Cursor Chat)- 定期检查并执行pending任务'
  559. )
  560. parser.add_argument(
  561. '--once',
  562. action='store_true',
  563. help='只执行一次,不循环'
  564. )
  565. parser.add_argument(
  566. '--interval',
  567. type=int,
  568. default=300,
  569. help='检查间隔(秒),默认300秒(5分钟)'
  570. )
  571. parser.add_argument(
  572. '--enable-chat',
  573. action='store_true',
  574. help='启用自动 Cursor Chat,在有pending任务时自动向 Cursor 发送提醒消息'
  575. )
  576. parser.add_argument(
  577. '--chat-input-pos',
  578. type=str,
  579. default=None,
  580. help='Cursor Chat 输入框位置,格式 "x,y"(例如 "1180,965"),不指定则自动尝试定位'
  581. )
  582. args = parser.parse_args()
  583. # 设置全局 Chat 配置
  584. global ENABLE_CHAT, CHAT_INPUT_POS # noqa: PLW0603
  585. ENABLE_CHAT = bool(args.enable_chat)
  586. CHAT_INPUT_POS = None
  587. if args.chat_input_pos:
  588. try:
  589. x_str, y_str = args.chat_input_pos.split(',')
  590. CHAT_INPUT_POS = (int(x_str.strip()), int(y_str.strip()))
  591. logger.info("使用指定的 Chat 输入框位置: %s", CHAT_INPUT_POS)
  592. except Exception as exc: # noqa: BLE001
  593. logger.warning("解析 --chat-input-pos 失败 %r: %s", args.chat_input_pos, exc)
  594. if args.once:
  595. # 执行一次
  596. count = auto_execute_tasks_once()
  597. if count > 0:
  598. send_chat_for_pending_tasks()
  599. logger.info(f"\n✅ 完成!处理了 {count} 个任务")
  600. else:
  601. # 循环执行
  602. auto_execute_tasks_loop(interval=args.interval)
  603. if __name__ == '__main__':
  604. main()