auto_execute_tasks.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. #!/usr/bin/env python3
  2. """
  3. 自动任务执行核心调度脚本(简化版)
  4. 工作流程:
  5. 1. 从 PostgreSQL 数据库 task_list 表中读取 pending 任务
  6. 2. 生成 .cursor/task_execute_instructions.md 执行指令文件
  7. 3. 更新任务状态为 processing,并维护 .cursor/pending_tasks.json
  8. 4. (可选)向 Cursor Chat 发送执行提醒
  9. 5. Cursor 完成任务后,将 pending_tasks.json 中的状态改为 completed
  10. 6. 调度脚本将 completed 状态的任务同步回数据库
  11. 使用方式:
  12. python scripts/auto_execute_tasks.py --once
  13. python scripts/auto_execute_tasks.py --interval 300
  14. python scripts/auto_execute_tasks.py --once --enable-chat
  15. """
  16. from __future__ import annotations
  17. import json
  18. import time
  19. import argparse
  20. import logging
  21. from pathlib import Path
  22. from datetime import datetime
  23. from typing import Any, Dict, List, Optional, Tuple
  24. # ============================================================================
  25. # 日志配置
  26. # ============================================================================
  27. logging.basicConfig(
  28. level=logging.INFO,
  29. format="%(asctime)s - %(levelname)s - %(message)s",
  30. )
  31. logger = logging.getLogger("AutoExecuteTasks")
  32. # ============================================================================
  33. # Windows GUI 自动化依赖(可选)
  34. # ============================================================================
  35. HAS_CURSOR_GUI = False
  36. HAS_PYPERCLIP = False
  37. try:
  38. import win32gui
  39. import win32con
  40. import pyautogui
  41. pyautogui.FAILSAFE = True
  42. pyautogui.PAUSE = 0.5
  43. HAS_CURSOR_GUI = True
  44. try:
  45. import pyperclip
  46. HAS_PYPERCLIP = True
  47. except ImportError:
  48. pass
  49. except ImportError:
  50. logger.info(
  51. "未安装 Windows GUI 自动化依赖(pywin32/pyautogui),"
  52. "将禁用自动 Cursor Chat 功能。"
  53. )
  54. # ============================================================================
  55. # 全局配置
  56. # ============================================================================
  57. WORKSPACE_ROOT = Path(__file__).parent.parent
  58. CURSOR_DIR = WORKSPACE_ROOT / ".cursor"
  59. PENDING_TASKS_FILE = CURSOR_DIR / "pending_tasks.json"
  60. INSTRUCTIONS_FILE = CURSOR_DIR / "task_execute_instructions.md"
  61. # 命令行参数控制的全局变量
  62. ENABLE_CHAT: bool = False
  63. CHAT_MESSAGE: str = "请阅读 .cursor/task_execute_instructions.md 并执行任务。"
  64. CHAT_INPUT_POS: Optional[Tuple[int, int]] = None
  65. # ============================================================================
  66. # 数据库操作
  67. # ============================================================================
  68. def get_db_connection():
  69. """获取数据库连接"""
  70. try:
  71. import psycopg2
  72. import sys
  73. sys.path.insert(0, str(WORKSPACE_ROOT))
  74. from app.config.config import config, current_env
  75. app_config = config.get(current_env, config['default'])
  76. db_uri = app_config.SQLALCHEMY_DATABASE_URI
  77. return psycopg2.connect(db_uri)
  78. except ImportError as e:
  79. logger.error(f"导入依赖失败: {e}")
  80. return None
  81. except Exception as e:
  82. logger.error(f"连接数据库失败: {e}")
  83. return None
  84. def get_pending_tasks() -> List[Dict[str, Any]]:
  85. """从数据库获取所有 pending 任务"""
  86. try:
  87. from psycopg2.extras import RealDictCursor
  88. conn = get_db_connection()
  89. if not conn:
  90. return []
  91. cursor = conn.cursor(cursor_factory=RealDictCursor)
  92. cursor.execute("""
  93. SELECT task_id, task_name, task_description, status,
  94. code_name, code_path, create_time, create_by
  95. FROM task_list
  96. WHERE status = 'pending'
  97. ORDER BY create_time ASC
  98. """)
  99. tasks = cursor.fetchall()
  100. cursor.close()
  101. conn.close()
  102. return [dict(task) for task in tasks]
  103. except Exception as e:
  104. logger.error(f"获取 pending 任务失败: {e}")
  105. return []
  106. def update_task_status(
  107. task_id: int,
  108. status: str,
  109. code_name: Optional[str] = None,
  110. code_path: Optional[str] = None,
  111. ) -> bool:
  112. """更新任务状态"""
  113. try:
  114. conn = get_db_connection()
  115. if not conn:
  116. return False
  117. cursor = conn.cursor()
  118. if code_name and code_path:
  119. cursor.execute("""
  120. UPDATE task_list
  121. SET status = %s, code_name = %s, code_path = %s,
  122. update_time = CURRENT_TIMESTAMP
  123. WHERE task_id = %s
  124. """, (status, code_name, code_path, task_id))
  125. else:
  126. cursor.execute("""
  127. UPDATE task_list
  128. SET status = %s, update_time = CURRENT_TIMESTAMP
  129. WHERE task_id = %s
  130. """, (status, task_id))
  131. conn.commit()
  132. updated = cursor.rowcount > 0
  133. cursor.close()
  134. conn.close()
  135. if updated:
  136. logger.info(f"✅ 任务 {task_id} 状态已更新为: {status}")
  137. return updated
  138. except Exception as e:
  139. logger.error(f"更新任务状态失败: {e}")
  140. return False
  141. # ============================================================================
  142. # 任务文件生成
  143. # ============================================================================
  144. def write_pending_tasks_json(tasks: List[Dict[str, Any]]) -> None:
  145. """将任务列表写入 .cursor/pending_tasks.json"""
  146. CURSOR_DIR.mkdir(parents=True, exist_ok=True)
  147. # 读取现有任务
  148. existing_tasks = []
  149. if PENDING_TASKS_FILE.exists():
  150. try:
  151. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  152. existing_tasks = json.load(f)
  153. except Exception:
  154. existing_tasks = []
  155. existing_ids = {t["task_id"] for t in existing_tasks if "task_id" in t}
  156. # 添加新任务
  157. for task in tasks:
  158. if task["task_id"] not in existing_ids:
  159. create_time = task.get("create_time", "")
  160. if hasattr(create_time, "isoformat"):
  161. create_time = create_time.isoformat()
  162. task_info = {
  163. "task_id": task["task_id"],
  164. "task_name": task["task_name"],
  165. "task_description": task["task_description"],
  166. "code_path": task.get("code_path", ""),
  167. "code_name": task.get("code_name", ""),
  168. "status": "processing",
  169. "notified_at": datetime.now().isoformat(),
  170. }
  171. existing_tasks.append(task_info)
  172. with PENDING_TASKS_FILE.open("w", encoding="utf-8") as f:
  173. json.dump(existing_tasks, f, indent=2, ensure_ascii=False)
  174. logger.info(f"✅ pending_tasks.json 已更新,任务数: {len(existing_tasks)}")
  175. def create_execute_instructions(tasks: List[Dict[str, Any]]) -> None:
  176. """生成任务执行指令文件 .cursor/task_execute_instructions.md"""
  177. CURSOR_DIR.mkdir(parents=True, exist_ok=True)
  178. with INSTRUCTIONS_FILE.open("w", encoding="utf-8") as f:
  179. f.write("# 🤖 Cursor 自动任务执行指令\n\n")
  180. f.write("**⚠️ 重要:请立即执行以下任务!**\n\n")
  181. gen_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  182. f.write(f"**生成时间**: {gen_time}\n\n")
  183. f.write(f"**待执行任务数量**: {len(tasks)}\n\n")
  184. f.write("## 📋 任务完成后的操作\n\n")
  185. f.write("完成每个任务后,请更新 `.cursor/pending_tasks.json` 中")
  186. f.write("对应任务的 `status` 为 `completed`,\n")
  187. f.write("并填写 `code_name`(代码文件名)和 `code_path`(代码路径)。\n\n")
  188. f.write("调度脚本会自动将完成的任务同步到数据库。\n\n")
  189. f.write("---\n\n")
  190. for idx, task in enumerate(tasks, 1):
  191. task_id = task['task_id']
  192. task_name = task['task_name']
  193. task_desc = task['task_description']
  194. create_time = task.get("create_time", "")
  195. if hasattr(create_time, "strftime"):
  196. create_time = create_time.strftime("%Y-%m-%d %H:%M:%S")
  197. f.write(f"## 🔴 任务 {idx}: {task_name}\n\n")
  198. f.write(f"- **任务ID**: `{task_id}`\n")
  199. f.write(f"- **创建时间**: {create_time}\n")
  200. f.write(f"- **创建者**: {task.get('create_by', 'unknown')}\n\n")
  201. f.write(f"### 📝 任务描述\n\n{task_desc}\n\n")
  202. f.write("---\n\n")
  203. logger.info(f"✅ 执行指令文件已创建: {INSTRUCTIONS_FILE}")
  204. # ============================================================================
  205. # 状态同步
  206. # ============================================================================
  207. def sync_completed_tasks_to_db() -> int:
  208. """将 pending_tasks.json 中 completed 的任务同步到数据库"""
  209. if not PENDING_TASKS_FILE.exists():
  210. return 0
  211. try:
  212. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  213. tasks = json.load(f)
  214. except Exception as e:
  215. logger.error(f"读取 pending_tasks.json 失败: {e}")
  216. return 0
  217. if not isinstance(tasks, list):
  218. return 0
  219. updated = 0
  220. remaining_tasks = []
  221. for t in tasks:
  222. if t.get("status") == "completed":
  223. task_id = t.get("task_id")
  224. if not task_id:
  225. continue
  226. code_name = t.get("code_name")
  227. code_path = t.get("code_path")
  228. if update_task_status(task_id, "completed", code_name, code_path):
  229. updated += 1
  230. logger.info(f"已同步任务 {task_id} 为 completed")
  231. else:
  232. remaining_tasks.append(t)
  233. else:
  234. remaining_tasks.append(t)
  235. if updated > 0:
  236. with PENDING_TASKS_FILE.open("w", encoding="utf-8") as f:
  237. json.dump(remaining_tasks, f, indent=2, ensure_ascii=False)
  238. logger.info(f"本次共同步 {updated} 个 completed 任务到数据库")
  239. return updated
  240. # ============================================================================
  241. # Cursor Chat 自动化
  242. # ============================================================================
  243. def find_cursor_window() -> Optional[int]:
  244. """查找 Cursor 主窗口句柄"""
  245. if not HAS_CURSOR_GUI:
  246. return None
  247. cursor_windows: List[Dict[str, Any]] = []
  248. def enum_windows_callback(hwnd, _extra):
  249. if win32gui.IsWindowVisible(hwnd):
  250. title = win32gui.GetWindowText(hwnd) or ""
  251. class_name = win32gui.GetClassName(hwnd) or ""
  252. is_cursor = "cursor" in title.lower()
  253. if class_name and "chrome_widgetwin" in class_name.lower():
  254. is_cursor = True
  255. if is_cursor:
  256. left, top, right, bottom = win32gui.GetWindowRect(hwnd)
  257. area = (right - left) * (bottom - top)
  258. cursor_windows.append({"hwnd": hwnd, "area": area})
  259. return True
  260. win32gui.EnumWindows(enum_windows_callback, None)
  261. if not cursor_windows:
  262. logger.warning("未找到 Cursor 窗口")
  263. return None
  264. cursor_windows.sort(key=lambda x: x["area"], reverse=True)
  265. return cursor_windows[0]["hwnd"]
  266. def send_chat_message(
  267. message: str, input_pos: Optional[Tuple[int, int]]
  268. ) -> bool:
  269. """在 Cursor Chat 中发送消息"""
  270. if not HAS_CURSOR_GUI:
  271. logger.warning("当前环境不支持 Cursor GUI 自动化")
  272. return False
  273. hwnd = find_cursor_window()
  274. if not hwnd:
  275. return False
  276. try:
  277. win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
  278. time.sleep(0.3)
  279. win32gui.SetForegroundWindow(hwnd)
  280. time.sleep(0.5)
  281. except Exception as e:
  282. logger.error(f"激活 Cursor 窗口失败: {e}")
  283. return False
  284. # 点击输入框或使用快捷键
  285. if input_pos:
  286. x, y = input_pos
  287. pyautogui.click(x, y)
  288. time.sleep(0.4)
  289. else:
  290. pyautogui.hotkey("ctrl", "l")
  291. time.sleep(1.0)
  292. pyautogui.hotkey("ctrl", "a")
  293. time.sleep(0.2)
  294. # 输入消息
  295. if HAS_PYPERCLIP:
  296. try:
  297. pyperclip.copy(message)
  298. pyautogui.hotkey("ctrl", "v")
  299. time.sleep(0.5)
  300. except Exception:
  301. pyautogui.write(message, interval=0.03)
  302. else:
  303. pyautogui.write(message, interval=0.03)
  304. time.sleep(0.3)
  305. pyautogui.press("enter")
  306. logger.info("✅ 消息已发送到 Cursor Chat")
  307. return True
  308. def send_chat_for_tasks() -> None:
  309. """向 Cursor Chat 发送任务提醒"""
  310. if not ENABLE_CHAT:
  311. return
  312. if not PENDING_TASKS_FILE.exists():
  313. return
  314. try:
  315. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  316. data = json.load(f)
  317. if not any(t.get("status") == "processing" for t in data):
  318. return
  319. except Exception:
  320. return
  321. logger.info("发送任务提醒到 Cursor Chat...")
  322. send_chat_message(CHAT_MESSAGE, CHAT_INPUT_POS)
  323. # ============================================================================
  324. # 主执行流程
  325. # ============================================================================
  326. def auto_execute_tasks_once() -> int:
  327. """执行一次任务检查和处理"""
  328. # 1. 先同步已完成任务到数据库
  329. sync_completed_tasks_to_db()
  330. # 2. 获取 pending 任务
  331. logger.info("🔍 检查 pending 任务...")
  332. tasks = get_pending_tasks()
  333. if not tasks:
  334. logger.info("✅ 没有 pending 任务")
  335. return 0
  336. logger.info(f"📋 找到 {len(tasks)} 个 pending 任务")
  337. # 3. 更新任务状态为 processing
  338. for task in tasks:
  339. update_task_status(task["task_id"], "processing")
  340. # 4. 写入 pending_tasks.json
  341. write_pending_tasks_json(tasks)
  342. # 5. 生成执行指令文件
  343. create_execute_instructions(tasks)
  344. return len(tasks)
  345. def auto_execute_tasks_loop(interval: int = 300) -> None:
  346. """循环执行任务检查"""
  347. logger.info("=" * 60)
  348. logger.info("🚀 自动任务执行服务已启动")
  349. logger.info(f"⏰ 检查间隔: {interval} 秒")
  350. logger.info(f"💬 自动 Chat: {'已启用' if ENABLE_CHAT else '未启用'}")
  351. logger.info("按 Ctrl+C 停止服务")
  352. logger.info("=" * 60)
  353. try:
  354. while True:
  355. try:
  356. count = auto_execute_tasks_once()
  357. if count > 0:
  358. send_chat_for_tasks()
  359. logger.info(f"✅ 已处理 {count} 个任务")
  360. logger.info(f"⏳ {interval} 秒后再次检查...")
  361. time.sleep(interval)
  362. except KeyboardInterrupt:
  363. raise
  364. except Exception as e:
  365. logger.error(f"❌ 执行出错: {e}")
  366. time.sleep(interval)
  367. except KeyboardInterrupt:
  368. logger.info("\n⛔ 服务已停止")
  369. def main() -> None:
  370. """主函数"""
  371. parser = argparse.ArgumentParser(
  372. description="自动任务执行调度脚本",
  373. formatter_class=argparse.RawDescriptionHelpFormatter,
  374. )
  375. parser.add_argument(
  376. "--once", action="store_true", help="只执行一次"
  377. )
  378. parser.add_argument(
  379. "--interval", type=int, default=300, help="检查间隔(秒)"
  380. )
  381. parser.add_argument(
  382. "--enable-chat", action="store_true", help="启用自动 Cursor Chat"
  383. )
  384. parser.add_argument(
  385. "--chat-input-pos", type=str, help='Chat 输入框位置 "x,y"'
  386. )
  387. parser.add_argument(
  388. "--chat-message", type=str,
  389. default="请阅读 .cursor/task_execute_instructions.md 并执行任务。",
  390. help="发送到 Chat 的消息"
  391. )
  392. args = parser.parse_args()
  393. global ENABLE_CHAT, CHAT_INPUT_POS, CHAT_MESSAGE
  394. ENABLE_CHAT = bool(args.enable_chat)
  395. CHAT_MESSAGE = args.chat_message
  396. if args.chat_input_pos:
  397. try:
  398. x, y = args.chat_input_pos.split(",")
  399. CHAT_INPUT_POS = (int(x.strip()), int(y.strip()))
  400. except Exception:
  401. pass
  402. if args.once:
  403. count = auto_execute_tasks_once()
  404. if count > 0:
  405. send_chat_for_tasks()
  406. logger.info(f"✅ 完成!处理了 {count} 个任务")
  407. else:
  408. auto_execute_tasks_loop(interval=args.interval)
  409. if __name__ == "__main__":
  410. main()