auto_execute_tasks.py 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743
  1. #!/usr/bin/env python3
  2. """
  3. 自动任务执行核心调度脚本
  4. 工作流程:
  5. 1. 从 PostgreSQL 数据库 task_list 表中读取 pending 任务
  6. 2. 生成 tasks/task_execute_instructions.md 执行指令文件
  7. 3. 更新任务状态为 processing,并维护 tasks/pending_tasks.json
  8. 4. 更新 tasks/task_trigger.txt 触发器文件
  9. 5. 启动新的 Cursor Agent 并发送执行指令
  10. 6. Cursor Agent 完成任务后,更新 pending_tasks.json 状态为 completed
  11. 7. 调度脚本检测到任务完成后,同步数据库并关闭 Agent
  12. 使用方式:
  13. # 执行一次任务检查
  14. python scripts/auto_execute_tasks.py --once
  15. # 循环模式(每 5 分钟检查)
  16. python scripts/auto_execute_tasks.py --interval 300
  17. # 刷新触发器文件(用于现有 processing 任务)
  18. python scripts/auto_execute_tasks.py --refresh-trigger
  19. # 【推荐】使用 Agent 模式执行任务(自动打开/关闭 Agent)
  20. python scripts/auto_execute_tasks.py --agent-run
  21. # 【推荐】启动 Agent 循环模式(有任务时自动启动 Agent,完成后自动关闭)
  22. python scripts/auto_execute_tasks.py --chat-loop --use-agent
  23. # 启动传统 Chat 模式(不使用 Agent)
  24. python scripts/auto_execute_tasks.py --chat-loop --no-agent
  25. # 立即发送 Chat 消息触发 Cursor 执行
  26. python scripts/auto_execute_tasks.py --send-chat-now
  27. # 设置 Agent 超时时间(默认 3600 秒)
  28. python scripts/auto_execute_tasks.py --agent-run --agent-timeout 7200
  29. # 任务完成后不自动关闭 Agent
  30. python scripts/auto_execute_tasks.py --agent-run --no-auto-close
  31. # 禁用自动部署功能
  32. python scripts/auto_execute_tasks.py --chat-loop --use-agent --no-deploy
  33. # 立即部署指定任务ID的脚本到生产服务器
  34. python scripts/auto_execute_tasks.py --deploy-now 123
  35. # 测试到生产服务器的 SSH 连接
  36. python scripts/auto_execute_tasks.py --test-connection
  37. """
  38. from __future__ import annotations
  39. import argparse
  40. import json
  41. import logging
  42. import time
  43. from datetime import datetime
  44. from pathlib import Path
  45. from typing import Any
  46. # ============================================================================
  47. # 日志配置
  48. # ============================================================================
  49. logging.basicConfig(
  50. level=logging.INFO,
  51. format="%(asctime)s - %(levelname)s - %(message)s",
  52. )
  53. logger = logging.getLogger("AutoExecuteTasks")
  54. # ============================================================================
  55. # Windows GUI 自动化依赖(可选)
  56. # ============================================================================
  57. HAS_CURSOR_GUI = False
  58. HAS_PYPERCLIP = False
  59. try:
  60. import pyautogui
  61. import win32con
  62. import win32gui
  63. pyautogui.FAILSAFE = True
  64. pyautogui.PAUSE = 0.5
  65. HAS_CURSOR_GUI = True
  66. try:
  67. import pyperclip
  68. HAS_PYPERCLIP = True
  69. except ImportError:
  70. pass
  71. except ImportError:
  72. logger.info(
  73. "未安装 Windows GUI 自动化依赖(pywin32/pyautogui),"
  74. "将禁用自动 Cursor Chat 功能。"
  75. )
  76. # ============================================================================
  77. # 全局配置
  78. # ============================================================================
  79. WORKSPACE_ROOT = Path(__file__).parent.parent
  80. TASKS_DIR = WORKSPACE_ROOT / "tasks"
  81. PENDING_TASKS_FILE = TASKS_DIR / "pending_tasks.json"
  82. INSTRUCTIONS_FILE = TASKS_DIR / "task_execute_instructions.md"
  83. TRIGGER_FILE = TASKS_DIR / "task_trigger.txt"
  84. # 生产服务器配置
  85. PRODUCTION_SERVER = {
  86. "host": "192.168.3.143",
  87. "port": 22,
  88. "username": "ubuntu",
  89. "password": "citumxl2357",
  90. "script_path": "/opt/dataops-platform/datafactory/scripts",
  91. "workflow_path": "/opt/dataops-platform/n8n/workflows",
  92. }
  93. # 命令行参数控制的全局变量
  94. ENABLE_CHAT: bool = False
  95. CHAT_MESSAGE: str = "请阅读 tasks/task_execute_instructions.md 并执行任务。"
  96. CHAT_INPUT_POS: tuple[int, int] | None = None
  97. ENABLE_AUTO_DEPLOY: bool = True # 默认启用自动部署
  98. # ============================================================================
  99. # 数据库操作
  100. # ============================================================================
  101. def get_db_connection():
  102. """获取数据库连接(使用 production 环境配置)"""
  103. try:
  104. import sys
  105. import psycopg2
  106. sys.path.insert(0, str(WORKSPACE_ROOT))
  107. from app.config.config import config
  108. # 强制使用 production 环境的数据库配置
  109. app_config = config["production"]
  110. db_uri = app_config.SQLALCHEMY_DATABASE_URI
  111. return psycopg2.connect(db_uri)
  112. except ImportError as e:
  113. logger.error(f"导入依赖失败: {e}")
  114. return None
  115. except Exception as e:
  116. logger.error(f"连接数据库失败: {e}")
  117. return None
  118. def get_pending_tasks() -> list[dict[str, Any]]:
  119. """从数据库获取所有 pending 任务"""
  120. try:
  121. from psycopg2.extras import RealDictCursor
  122. conn = get_db_connection()
  123. if not conn:
  124. return []
  125. cursor = conn.cursor(cursor_factory=RealDictCursor)
  126. cursor.execute("""
  127. SELECT task_id, task_name, task_description, status,
  128. code_name, code_path, create_time, create_by
  129. FROM task_list
  130. WHERE status = 'pending'
  131. ORDER BY create_time ASC
  132. """)
  133. tasks = cursor.fetchall()
  134. cursor.close()
  135. conn.close()
  136. return [dict(task) for task in tasks]
  137. except Exception as e:
  138. logger.error(f"获取 pending 任务失败: {e}")
  139. return []
  140. def update_task_status(
  141. task_id: int,
  142. status: str,
  143. code_name: str | None = None,
  144. code_path: str | None = None,
  145. ) -> bool:
  146. """更新任务状态"""
  147. try:
  148. conn = get_db_connection()
  149. if not conn:
  150. return False
  151. cursor = conn.cursor()
  152. if code_name and code_path:
  153. cursor.execute(
  154. """
  155. UPDATE task_list
  156. SET status = %s, code_name = %s, code_path = %s,
  157. update_time = CURRENT_TIMESTAMP
  158. WHERE task_id = %s
  159. """,
  160. (status, code_name, code_path, task_id),
  161. )
  162. else:
  163. cursor.execute(
  164. """
  165. UPDATE task_list
  166. SET status = %s, update_time = CURRENT_TIMESTAMP
  167. WHERE task_id = %s
  168. """,
  169. (status, task_id),
  170. )
  171. conn.commit()
  172. updated = cursor.rowcount > 0
  173. cursor.close()
  174. conn.close()
  175. if updated:
  176. logger.info(f"✅ 任务 {task_id} 状态已更新为: {status}")
  177. return updated
  178. except Exception as e:
  179. logger.error(f"更新任务状态失败: {e}")
  180. return False
  181. # ============================================================================
  182. # 触发器文件管理
  183. # ============================================================================
  184. def update_trigger_file(
  185. task_count: int = 0,
  186. status: str = "有待执行任务",
  187. task_ids: list[int] | None = None,
  188. ) -> None:
  189. """
  190. 更新触发器文件 tasks/task_trigger.txt
  191. Cursor Rules 会检测此文件的更新时间,如果在 5 分钟内更新,
  192. 则触发自动任务执行。
  193. """
  194. TASKS_DIR.mkdir(parents=True, exist_ok=True)
  195. gen_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  196. task_ids_str = ", ".join(str(tid) for tid in (task_ids or []))
  197. content = f"""CURSOR_AUTO_EXECUTE_TASK_TRIGGER
  198. 生成时间: {gen_time}
  199. 状态: {status}
  200. 待处理任务数: {task_count}
  201. 任务ID列表: [{task_ids_str}]
  202. 此文件用于触发Cursor自动执行任务。
  203. 当此文件更新时间在5分钟内时,Cursor应该检查 tasks/task_execute_instructions.md 并执行其中的任务。
  204. 【重要】Cursor AI 请立即执行以下操作:
  205. 1. 读取 tasks/task_execute_instructions.md 文件
  206. 2. 按照文件中的任务描述,依次完成每个任务
  207. 3. 完成后更新 tasks/pending_tasks.json 中对应任务的状态为 completed
  208. """
  209. with TRIGGER_FILE.open("w", encoding="utf-8") as f:
  210. f.write(content)
  211. logger.info(f"✅ 触发器文件已更新: {TRIGGER_FILE}")
  212. def get_processing_task_ids() -> list[int]:
  213. """从 pending_tasks.json 获取所有 processing 状态的任务 ID"""
  214. if not PENDING_TASKS_FILE.exists():
  215. return []
  216. try:
  217. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  218. tasks = json.load(f)
  219. return [
  220. t.get("task_id")
  221. for t in tasks
  222. if t.get("status") == "processing" and t.get("task_id")
  223. ]
  224. except Exception:
  225. return []
  226. # ============================================================================
  227. # 任务文件生成
  228. # ============================================================================
  229. def write_pending_tasks_json(tasks: list[dict[str, Any]]) -> None:
  230. """将任务列表写入 tasks/pending_tasks.json"""
  231. TASKS_DIR.mkdir(parents=True, exist_ok=True)
  232. # 读取现有任务
  233. existing_tasks = []
  234. if PENDING_TASKS_FILE.exists():
  235. try:
  236. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  237. existing_tasks = json.load(f)
  238. except Exception:
  239. existing_tasks = []
  240. existing_ids = {t["task_id"] for t in existing_tasks if "task_id" in t}
  241. # 添加新任务
  242. for task in tasks:
  243. if task["task_id"] not in existing_ids:
  244. task_info = {
  245. "task_id": task["task_id"],
  246. "task_name": task["task_name"],
  247. "code_path": task.get("code_path", ""),
  248. "code_name": task.get("code_name", ""),
  249. "status": "processing",
  250. "notified_at": datetime.now().isoformat(),
  251. "code_file": task.get("code_file", ""),
  252. }
  253. existing_tasks.append(task_info)
  254. with PENDING_TASKS_FILE.open("w", encoding="utf-8") as f:
  255. json.dump(existing_tasks, f, indent=2, ensure_ascii=False)
  256. logger.info(f"✅ pending_tasks.json 已更新,任务数: {len(existing_tasks)}")
  257. def create_execute_instructions(tasks: list[dict[str, Any]]) -> None:
  258. """生成任务执行指令文件 tasks/task_execute_instructions.md"""
  259. TASKS_DIR.mkdir(parents=True, exist_ok=True)
  260. with INSTRUCTIONS_FILE.open("w", encoding="utf-8") as f:
  261. f.write("# 🤖 Cursor 自动任务执行指令\n\n")
  262. f.write("**⚠️ 重要:请立即执行以下任务!**\n\n")
  263. gen_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  264. f.write(f"**生成时间**: {gen_time}\n\n")
  265. f.write(f"**待执行任务数量**: {len(tasks)}\n\n")
  266. f.write("## 📋 任务完成后的操作\n\n")
  267. f.write("完成每个任务后,请更新 `tasks/pending_tasks.json` 中")
  268. f.write("对应任务的 `status` 为 `completed`,\n")
  269. f.write("并填写 `code_name`(代码文件名)和 `code_path`(代码路径)。\n\n")
  270. f.write("调度脚本会自动将完成的任务同步到数据库。\n\n")
  271. f.write("## ⚠️ 任务约束要求\n\n")
  272. f.write("**重要约束**:完成脚本创建后,**不需要生成任务总结文件**。\n\n")
  273. f.write("- ❌ 不要创建任何 summary、report、总结类的文档文件\n")
  274. f.write("- ❌ 不要生成 task_summary.md、execution_report.md 等总结文件\n")
  275. f.write("- ✅ 只需创建任务要求的功能脚本文件\n")
  276. f.write("- ✅ 只需更新 `tasks/pending_tasks.json` 中的任务状态\n\n")
  277. f.write("---\n\n")
  278. for idx, task in enumerate(tasks, 1):
  279. task_id = task["task_id"]
  280. task_name = task["task_name"]
  281. task_desc = task["task_description"]
  282. create_time = task.get("create_time", "")
  283. if hasattr(create_time, "strftime"):
  284. create_time = create_time.strftime("%Y-%m-%d %H:%M:%S")
  285. f.write(f"## 🔴 任务 {idx}: {task_name}\n\n")
  286. f.write(f"- **任务ID**: `{task_id}`\n")
  287. f.write(f"- **创建时间**: {create_time}\n")
  288. f.write(f"- **创建者**: {task.get('create_by', 'unknown')}\n\n")
  289. f.write(f"### 📝 任务描述\n\n{task_desc}\n\n")
  290. f.write("---\n\n")
  291. logger.info(f"✅ 执行指令文件已创建: {INSTRUCTIONS_FILE}")
  292. # ============================================================================
  293. # Neo4j 独立连接(不依赖 Flask 应用上下文)
  294. # ============================================================================
  295. def get_neo4j_driver():
  296. """获取 Neo4j 驱动(独立于 Flask 应用上下文)"""
  297. try:
  298. import sys
  299. from neo4j import GraphDatabase
  300. sys.path.insert(0, str(WORKSPACE_ROOT))
  301. from app.config.config import config
  302. # 强制使用 production 环境的配置
  303. app_config = config["production"]
  304. uri = app_config.NEO4J_URI
  305. user = app_config.NEO4J_USER
  306. password = app_config.NEO4J_PASSWORD
  307. driver = GraphDatabase.driver(uri, auth=(user, password))
  308. return driver
  309. except ImportError as e:
  310. logger.error(f"导入 Neo4j 驱动失败: {e}")
  311. return None
  312. except Exception as e:
  313. logger.error(f"连接 Neo4j 失败: {e}")
  314. return None
  315. # ============================================================================
  316. # 状态同步
  317. # ============================================================================
  318. def update_dataflow_script_path(task_name: str, script_path: str) -> bool:
  319. """
  320. 更新 DataFlow 节点的 script_path 字段(独立于 Flask 应用上下文)
  321. Args:
  322. task_name: 任务名称(对应 DataFlow 的 name_zh)
  323. script_path: Python 脚本的完整路径
  324. Returns:
  325. 是否更新成功
  326. """
  327. try:
  328. from datetime import datetime
  329. driver = get_neo4j_driver()
  330. if not driver:
  331. logger.error("无法获取 Neo4j 驱动")
  332. return False
  333. query = """
  334. MATCH (n:DataFlow {name_zh: $name_zh})
  335. SET n.script_path = $script_path, n.updated_at = $updated_at
  336. RETURN n
  337. """
  338. updated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  339. with driver.session() as session:
  340. result = session.run(
  341. query,
  342. name_zh=task_name,
  343. script_path=script_path,
  344. updated_at=updated_at,
  345. ).single()
  346. driver.close()
  347. if result:
  348. logger.info(f"成功更新 DataFlow 脚本路径: {task_name} -> {script_path}")
  349. return True
  350. else:
  351. logger.warning(f"未找到 DataFlow 节点: {task_name}")
  352. return False
  353. except Exception as e:
  354. logger.error(f"更新 DataFlow script_path 失败: {e}")
  355. return False
  356. def sync_completed_tasks_to_db() -> int:
  357. """将 pending_tasks.json 中 completed 的任务同步到数据库"""
  358. if not PENDING_TASKS_FILE.exists():
  359. return 0
  360. try:
  361. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  362. tasks = json.load(f)
  363. except Exception as e:
  364. logger.error(f"读取 pending_tasks.json 失败: {e}")
  365. return 0
  366. if not isinstance(tasks, list):
  367. return 0
  368. updated = 0
  369. remaining_tasks = []
  370. for t in tasks:
  371. if t.get("status") == "completed":
  372. task_id = t.get("task_id")
  373. if not task_id:
  374. continue
  375. task_name = t.get("task_name")
  376. code_name = t.get("code_name")
  377. code_path = t.get("code_path")
  378. # 统一处理:code_path 始终为 "datafactory/scripts"
  379. # code_name 保持原样(可能是新建的脚本名或 import_resource_data.py)
  380. code_path = "datafactory/scripts"
  381. # 只处理 Python 脚本文件,跳过 JSON 等其他文件
  382. is_python_script = code_name and code_name.endswith(".py")
  383. if is_python_script:
  384. logger.info(f"任务 {task_id} 使用 Python 脚本: {code_path}/{code_name}")
  385. else:
  386. logger.info(
  387. f"任务 {task_id} 的 code_name ({code_name}) 不是 Python 脚本,跳过 DataFlow 更新"
  388. )
  389. if update_task_status(task_id, "completed", code_name, code_path):
  390. updated += 1
  391. logger.info(f"已同步任务 {task_id} 为 completed")
  392. # 只有 Python 脚本才更新 DataFlow 节点的 script_path
  393. if task_name and is_python_script:
  394. full_script_path = f"{code_path}/{code_name}"
  395. if update_dataflow_script_path(task_name, full_script_path):
  396. logger.info(
  397. f"已更新 DataFlow 脚本路径: {task_name} -> {full_script_path}"
  398. )
  399. else:
  400. logger.warning(f"更新 DataFlow 脚本路径失败: {task_name}")
  401. # 自动部署到生产服务器(如果启用)
  402. if ENABLE_AUTO_DEPLOY:
  403. logger.info(f"🚀 开始自动部署任务 {task_id} 到生产服务器...")
  404. if auto_deploy_completed_task(t):
  405. logger.info(f"✅ 任务 {task_id} 已成功部署到生产服务器")
  406. else:
  407. logger.warning(f"⚠️ 任务 {task_id} 部署到生产服务器失败")
  408. else:
  409. logger.info(f"ℹ️ 自动部署已禁用,跳过任务 {task_id} 的部署")
  410. else:
  411. remaining_tasks.append(t)
  412. else:
  413. remaining_tasks.append(t)
  414. if updated > 0:
  415. with PENDING_TASKS_FILE.open("w", encoding="utf-8") as f:
  416. json.dump(remaining_tasks, f, indent=2, ensure_ascii=False)
  417. logger.info(f"本次共同步 {updated} 个 completed 任务到数据库")
  418. return updated
  419. # ============================================================================
  420. # 生产服务器部署功能
  421. # ============================================================================
  422. def get_ssh_connection():
  423. """获取 SSH 连接到生产服务器"""
  424. try:
  425. import paramiko # type: ignore
  426. ssh = paramiko.SSHClient()
  427. ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  428. logger.info(
  429. f"正在连接生产服务器 {PRODUCTION_SERVER['username']}@"
  430. f"{PRODUCTION_SERVER['host']}:{PRODUCTION_SERVER['port']}..."
  431. )
  432. ssh.connect(
  433. hostname=PRODUCTION_SERVER["host"],
  434. port=PRODUCTION_SERVER["port"],
  435. username=PRODUCTION_SERVER["username"],
  436. password=PRODUCTION_SERVER["password"],
  437. timeout=10,
  438. )
  439. logger.info("✅ SSH 连接成功")
  440. return ssh
  441. except ImportError:
  442. logger.error("未安装 paramiko 库,请运行: pip install paramiko")
  443. return None
  444. except Exception as e:
  445. logger.error(f"SSH 连接失败: {e}")
  446. return None
  447. def test_ssh_connection() -> bool:
  448. """
  449. 测试 SSH 连接到生产服务器
  450. Returns:
  451. 连接是否成功
  452. """
  453. logger.info("=" * 60)
  454. logger.info("🔍 测试生产服务器连接")
  455. logger.info("=" * 60)
  456. ssh = get_ssh_connection()
  457. if not ssh:
  458. logger.error("❌ SSH 连接测试失败")
  459. return False
  460. try:
  461. # 测试执行命令
  462. _, stdout, _ = ssh.exec_command("echo 'Connection test successful'")
  463. output = stdout.read().decode().strip()
  464. logger.info(f"✅ 命令执行成功: {output}")
  465. # 检查目标目录是否存在
  466. _, stdout, _ = ssh.exec_command(
  467. f"test -d {PRODUCTION_SERVER['script_path']} && echo 'exists' || echo 'not exists'"
  468. )
  469. result = stdout.read().decode().strip()
  470. if result == "exists":
  471. logger.info(f"✅ 脚本目录存在: {PRODUCTION_SERVER['script_path']}")
  472. else:
  473. logger.warning(f"⚠️ 脚本目录不存在: {PRODUCTION_SERVER['script_path']}")
  474. logger.info("将在首次部署时自动创建")
  475. ssh.close()
  476. logger.info("=" * 60)
  477. logger.info("✅ 连接测试完成")
  478. logger.info("=" * 60)
  479. return True
  480. except Exception as e:
  481. logger.error(f"❌ 测试执行命令失败: {e}")
  482. ssh.close()
  483. return False
  484. def deploy_script_to_production(
  485. local_script_path: str, remote_filename: str | None = None
  486. ) -> bool:
  487. """
  488. 部署脚本文件到生产服务器
  489. Args:
  490. local_script_path: 本地脚本文件路径(相对或绝对)
  491. remote_filename: 远程文件名(可选,默认使用本地文件名)
  492. Returns:
  493. 是否部署成功
  494. """
  495. try:
  496. import importlib.util
  497. if importlib.util.find_spec("paramiko") is None:
  498. logger.error("未安装 paramiko 库,请运行: pip install paramiko")
  499. return False
  500. # 转换为绝对路径
  501. local_path = Path(local_script_path)
  502. if not local_path.is_absolute():
  503. local_path = WORKSPACE_ROOT / local_path
  504. if not local_path.exists():
  505. logger.error(f"本地文件不存在: {local_path}")
  506. return False
  507. # 确定远程文件名
  508. if not remote_filename:
  509. remote_filename = local_path.name
  510. remote_path = f"{PRODUCTION_SERVER['script_path']}/{remote_filename}"
  511. # 建立 SSH 连接
  512. ssh = get_ssh_connection()
  513. if not ssh:
  514. return False
  515. try:
  516. # 创建 SFTP 客户端
  517. sftp = ssh.open_sftp()
  518. # 确保远程目录存在
  519. try:
  520. sftp.stat(PRODUCTION_SERVER["script_path"])
  521. except FileNotFoundError:
  522. logger.info(f"创建远程目录: {PRODUCTION_SERVER['script_path']}")
  523. _, stdout, _ = ssh.exec_command(
  524. f"mkdir -p {PRODUCTION_SERVER['script_path']}"
  525. )
  526. stdout.channel.recv_exit_status()
  527. # 上传文件
  528. logger.info(f"正在上传: {local_path} -> {remote_path}")
  529. sftp.put(str(local_path), remote_path)
  530. # 设置文件权限为可执行
  531. sftp.chmod(remote_path, 0o755)
  532. logger.info(f"✅ 脚本部署成功: {remote_path}")
  533. sftp.close()
  534. ssh.close()
  535. return True
  536. except Exception as e:
  537. logger.error(f"文件传输失败: {e}")
  538. ssh.close()
  539. return False
  540. except ImportError:
  541. logger.error("未安装 paramiko 库,请运行: pip install paramiko")
  542. return False
  543. except Exception as e:
  544. logger.error(f"部署脚本失败: {e}")
  545. return False
  546. def deploy_n8n_workflow_to_production(workflow_file: str) -> bool:
  547. """
  548. 部署 n8n 工作流文件到生产服务器
  549. Args:
  550. workflow_file: 本地工作流 JSON 文件路径
  551. Returns:
  552. 是否部署成功
  553. """
  554. try:
  555. import importlib.util
  556. if importlib.util.find_spec("paramiko") is None:
  557. logger.error("未安装 paramiko 库,请运行: pip install paramiko")
  558. return False
  559. # 转换为绝对路径
  560. local_path = Path(workflow_file)
  561. if not local_path.is_absolute():
  562. local_path = WORKSPACE_ROOT / local_path
  563. if not local_path.exists():
  564. logger.error(f"工作流文件不存在: {local_path}")
  565. return False
  566. remote_path = f"{PRODUCTION_SERVER['workflow_path']}/{local_path.name}"
  567. # 建立 SSH 连接
  568. ssh = get_ssh_connection()
  569. if not ssh:
  570. return False
  571. try:
  572. # 创建 SFTP 客户端
  573. sftp = ssh.open_sftp()
  574. # 确保远程目录存在
  575. try:
  576. sftp.stat(PRODUCTION_SERVER["workflow_path"])
  577. except FileNotFoundError:
  578. logger.info(f"创建远程目录: {PRODUCTION_SERVER['workflow_path']}")
  579. _, stdout, _ = ssh.exec_command(
  580. f"mkdir -p {PRODUCTION_SERVER['workflow_path']}"
  581. )
  582. stdout.channel.recv_exit_status()
  583. # 上传工作流文件
  584. logger.info(f"正在上传工作流: {local_path} -> {remote_path}")
  585. sftp.put(str(local_path), remote_path)
  586. logger.info(f"✅ 工作流部署成功: {remote_path}")
  587. sftp.close()
  588. ssh.close()
  589. return True
  590. except Exception as e:
  591. logger.error(f"工作流传输失败: {e}")
  592. ssh.close()
  593. return False
  594. except ImportError:
  595. logger.error("未安装 paramiko 库,请运行: pip install paramiko")
  596. return False
  597. except Exception as e:
  598. logger.error(f"部署工作流失败: {e}")
  599. return False
  600. def auto_deploy_completed_task(task_info: dict[str, Any]) -> bool:
  601. """
  602. 自动部署已完成任务的脚本和工作流到生产服务器
  603. Args:
  604. task_info: 任务信息字典,包含 code_name, code_path 等
  605. Returns:
  606. 是否部署成功
  607. """
  608. code_name = task_info.get("code_name")
  609. code_path = task_info.get("code_path")
  610. task_name = task_info.get("task_name", "未知任务")
  611. if not code_name or not code_path:
  612. logger.warning(f"任务 {task_name} 缺少代码文件信息,跳过部署")
  613. return False
  614. logger.info("=" * 60)
  615. logger.info(f"🚀 开始自动部署任务: {task_name}")
  616. logger.info("=" * 60)
  617. deploy_success = True
  618. # 1. 部署 Python 脚本
  619. if code_name.endswith(".py"):
  620. script_path = f"{code_path}/{code_name}"
  621. logger.info(f"📦 部署 Python 脚本: {script_path}")
  622. if deploy_script_to_production(script_path):
  623. logger.info(f"✅ 脚本 {code_name} 部署成功")
  624. else:
  625. logger.error(f"❌ 脚本 {code_name} 部署失败")
  626. deploy_success = False
  627. # 2. 查找并部署相关的 n8n 工作流文件
  628. # 工作流文件通常与脚本在同一目录或 n8n_workflows 目录
  629. workflow_files = []
  630. # 查找模式1: 与脚本同目录的工作流文件
  631. script_dir = WORKSPACE_ROOT / code_path
  632. if script_dir.exists() and script_dir.is_dir():
  633. for wf_file in script_dir.glob("n8n_workflow_*.json"):
  634. if wf_file.is_file():
  635. workflow_files.append(wf_file)
  636. # 查找模式2: datafactory/n8n_workflows 目录
  637. n8n_workflows_dir = WORKSPACE_ROOT / "datafactory" / "n8n_workflows"
  638. if n8n_workflows_dir.exists():
  639. for wf_file in n8n_workflows_dir.glob("*.json"):
  640. if wf_file.is_file() and wf_file not in workflow_files:
  641. workflow_files.append(wf_file)
  642. # 查找模式3: 根据任务名称匹配工作流文件
  643. if task_name and task_name != "未知任务":
  644. # 将任务名称转换为可能的文件名模式
  645. task_name_pattern = task_name.replace(" ", "_").lower()
  646. for wf_file in (WORKSPACE_ROOT / "datafactory").rglob(
  647. f"*{task_name_pattern}*.json"
  648. ):
  649. if (
  650. wf_file.is_file()
  651. and "n8n" in wf_file.name.lower()
  652. and wf_file not in workflow_files
  653. ):
  654. workflow_files.append(wf_file)
  655. if workflow_files:
  656. logger.info(f"📦 发现 {len(workflow_files)} 个工作流文件")
  657. for wf_file in workflow_files:
  658. logger.info(f"📦 部署工作流: {wf_file.name}")
  659. if deploy_n8n_workflow_to_production(str(wf_file)):
  660. logger.info(f"✅ 工作流 {wf_file.name} 部署成功")
  661. else:
  662. logger.error(f"❌ 工作流 {wf_file.name} 部署失败")
  663. deploy_success = False
  664. else:
  665. logger.info("ℹ️ 未发现相关工作流文件")
  666. logger.info("=" * 60)
  667. if deploy_success:
  668. logger.info(f"✅ 任务 {task_name} 部署完成")
  669. else:
  670. logger.warning(f"⚠️ 任务 {task_name} 部署过程中出现错误")
  671. logger.info("=" * 60)
  672. return deploy_success
  673. # ============================================================================
  674. # Cursor Chat 自动化(Agent 模式)
  675. # ============================================================================
  676. # Agent 会话状态
  677. AGENT_SESSION_ACTIVE: bool = False
  678. AGENT_START_TIME: float = 0
  679. def get_all_cursor_windows() -> list[dict[str, Any]]:
  680. """获取所有 Cursor 窗口信息"""
  681. if not HAS_CURSOR_GUI:
  682. return []
  683. cursor_windows: list[dict[str, Any]] = []
  684. def enum_windows_callback(hwnd, _extra):
  685. if win32gui.IsWindowVisible(hwnd):
  686. title = win32gui.GetWindowText(hwnd) or ""
  687. class_name = win32gui.GetClassName(hwnd) or ""
  688. is_cursor = "cursor" in title.lower()
  689. if class_name and "chrome_widgetwin" in class_name.lower():
  690. is_cursor = True
  691. if is_cursor:
  692. left, top, right, bottom = win32gui.GetWindowRect(hwnd)
  693. area = (right - left) * (bottom - top)
  694. cursor_windows.append(
  695. {
  696. "hwnd": hwnd,
  697. "title": title,
  698. "class_name": class_name,
  699. "area": area,
  700. }
  701. )
  702. return True
  703. win32gui.EnumWindows(enum_windows_callback, None)
  704. return cursor_windows
  705. def find_cursor_window() -> int | None:
  706. """查找 Cursor 主窗口句柄"""
  707. if not HAS_CURSOR_GUI:
  708. return None
  709. cursor_windows = get_all_cursor_windows()
  710. if not cursor_windows:
  711. logger.warning("未找到 Cursor 窗口")
  712. return None
  713. # 按面积排序,返回最大的窗口(主窗口)
  714. cursor_windows.sort(key=lambda x: x["area"], reverse=True)
  715. return cursor_windows[0]["hwnd"]
  716. def activate_window(hwnd: int) -> bool:
  717. """激活指定窗口"""
  718. if not HAS_CURSOR_GUI:
  719. return False
  720. try:
  721. win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
  722. time.sleep(0.3)
  723. win32gui.SetForegroundWindow(hwnd)
  724. time.sleep(0.5)
  725. return True
  726. except Exception as e:
  727. logger.error(f"激活窗口失败: {e}")
  728. return False
  729. def open_new_agent() -> bool:
  730. """
  731. 在 Cursor 中打开新的 Agent 窗口
  732. 使用快捷键 Ctrl+Shift+I 打开新的 Composer/Agent
  733. """
  734. global AGENT_SESSION_ACTIVE, AGENT_START_TIME
  735. if not HAS_CURSOR_GUI:
  736. logger.warning("当前环境不支持 Cursor GUI 自动化")
  737. return False
  738. hwnd = find_cursor_window()
  739. if not hwnd:
  740. return False
  741. if not activate_window(hwnd):
  742. return False
  743. try:
  744. # 使用 Ctrl+Shift+I 打开新的 Agent/Composer
  745. # 这是 Cursor 默认的快捷键
  746. logger.info("🚀 正在打开新的 Agent...")
  747. pyautogui.hotkey("ctrl", "shift", "i")
  748. time.sleep(2.0) # 等待 Agent 窗口打开
  749. AGENT_SESSION_ACTIVE = True
  750. AGENT_START_TIME = time.time()
  751. logger.info("✅ 新的 Agent 已打开")
  752. return True
  753. except Exception as e:
  754. logger.error(f"打开 Agent 失败: {e}")
  755. return False
  756. def close_current_agent() -> bool:
  757. """
  758. 关闭当前的 Agent 会话
  759. 使用 Escape 键关闭 Agent 或使用快捷键
  760. """
  761. global AGENT_SESSION_ACTIVE
  762. if not HAS_CURSOR_GUI:
  763. return False
  764. if not AGENT_SESSION_ACTIVE:
  765. logger.info("没有活动的 Agent 会话")
  766. return True
  767. hwnd = find_cursor_window()
  768. if not hwnd:
  769. return False
  770. if not activate_window(hwnd):
  771. return False
  772. try:
  773. logger.info("🔄 正在关闭 Agent...")
  774. # 方法1: 按 Escape 键关闭 Agent
  775. pyautogui.press("escape")
  776. time.sleep(0.5)
  777. # 再按一次确保关闭
  778. pyautogui.press("escape")
  779. time.sleep(0.3)
  780. AGENT_SESSION_ACTIVE = False
  781. logger.info("✅ Agent 已关闭")
  782. return True
  783. except Exception as e:
  784. logger.error(f"关闭 Agent 失败: {e}")
  785. return False
  786. def type_message_to_agent(message: str) -> bool:
  787. """
  788. 向 Agent 输入消息
  789. Args:
  790. message: 要发送的消息内容
  791. Returns:
  792. 是否成功发送
  793. """
  794. if not HAS_CURSOR_GUI:
  795. return False
  796. try:
  797. # 等待 Agent 输入框获得焦点
  798. time.sleep(0.5)
  799. # 使用剪贴板粘贴(更可靠地处理中文和特殊字符)
  800. if HAS_PYPERCLIP:
  801. try:
  802. pyperclip.copy(message)
  803. pyautogui.hotkey("ctrl", "v")
  804. time.sleep(0.5)
  805. except Exception:
  806. # 回退到逐字符输入
  807. pyautogui.write(message, interval=0.03)
  808. else:
  809. pyautogui.write(message, interval=0.03)
  810. time.sleep(0.3)
  811. # 按 Enter 发送消息
  812. pyautogui.press("enter")
  813. logger.info("✅ 消息已发送到 Agent")
  814. return True
  815. except Exception as e:
  816. logger.error(f"发送消息到 Agent 失败: {e}")
  817. return False
  818. def wait_for_agent_completion(
  819. timeout: int = 3600,
  820. check_interval: int = 30,
  821. ) -> bool:
  822. """
  823. 等待 Agent 完成任务
  824. 通过检查 pending_tasks.json 中的任务状态来判断是否完成
  825. Args:
  826. timeout: 超时时间(秒),默认 1 小时
  827. check_interval: 检查间隔(秒),默认 30 秒
  828. Returns:
  829. 是否所有任务都已完成
  830. """
  831. start_time = time.time()
  832. logger.info(f"⏳ 等待 Agent 完成任务(超时: {timeout}s)...")
  833. while time.time() - start_time < timeout:
  834. processing_ids = get_processing_task_ids()
  835. if not processing_ids:
  836. elapsed = int(time.time() - start_time)
  837. logger.info(f"✅ 所有任务已完成!耗时: {elapsed}s")
  838. return True
  839. remaining = len(processing_ids)
  840. elapsed = int(time.time() - start_time)
  841. logger.info(
  842. f"⏳ 仍有 {remaining} 个任务进行中... (已等待 {elapsed}s / {timeout}s)"
  843. )
  844. time.sleep(check_interval)
  845. logger.warning("⚠️ 等待超时,仍有未完成的任务")
  846. return False
  847. def run_agent_session(
  848. message: str,
  849. wait_completion: bool = True,
  850. timeout: int = 3600,
  851. auto_close: bool = True,
  852. ) -> bool:
  853. """
  854. 运行完整的 Agent 会话
  855. 流程:
  856. 1. 打开新的 Agent
  857. 2. 发送任务消息
  858. 3. (可选)等待任务完成
  859. 4. (可选)关闭 Agent
  860. Args:
  861. message: 要发送给 Agent 的消息
  862. wait_completion: 是否等待任务完成
  863. timeout: 等待超时时间(秒)
  864. auto_close: 任务完成后是否自动关闭 Agent
  865. Returns:
  866. 是否成功完成会话
  867. """
  868. logger.info("=" * 50)
  869. logger.info("🤖 开始 Agent 会话")
  870. logger.info("=" * 50)
  871. # 1. 打开新的 Agent
  872. if not open_new_agent():
  873. logger.error("❌ 无法打开 Agent")
  874. return False
  875. # 2. 发送消息
  876. if not type_message_to_agent(message):
  877. logger.error("❌ 无法发送消息到 Agent")
  878. close_current_agent()
  879. return False
  880. logger.info(f"📤 已发送消息: {message[:50]}...")
  881. # 3. 等待任务完成
  882. if wait_completion:
  883. completed = wait_for_agent_completion(timeout=timeout)
  884. # 同步已完成的任务到数据库
  885. sync_completed_tasks_to_db()
  886. if completed:
  887. logger.info("✅ Agent 已完成所有任务")
  888. else:
  889. logger.warning("⚠️ Agent 未能在超时时间内完成所有任务")
  890. # 4. 关闭 Agent
  891. if auto_close:
  892. close_current_agent()
  893. logger.info("=" * 50)
  894. logger.info("🏁 Agent 会话结束")
  895. logger.info("=" * 50)
  896. return True
  897. def send_chat_message(message: str, input_pos: tuple[int, int] | None) -> bool:
  898. """
  899. 在 Cursor Chat 中发送消息(传统方式,保留向后兼容)
  900. 推荐使用 run_agent_session() 替代此函数
  901. """
  902. if not HAS_CURSOR_GUI:
  903. logger.warning("当前环境不支持 Cursor GUI 自动化")
  904. return False
  905. hwnd = find_cursor_window()
  906. if not hwnd:
  907. return False
  908. if not activate_window(hwnd):
  909. return False
  910. # 点击输入框或使用快捷键
  911. if input_pos:
  912. x, y = input_pos
  913. pyautogui.click(x, y)
  914. time.sleep(0.4)
  915. else:
  916. pyautogui.hotkey("ctrl", "l")
  917. time.sleep(1.0)
  918. pyautogui.hotkey("ctrl", "a")
  919. time.sleep(0.2)
  920. # 输入消息
  921. if HAS_PYPERCLIP:
  922. try:
  923. pyperclip.copy(message)
  924. pyautogui.hotkey("ctrl", "v")
  925. time.sleep(0.5)
  926. except Exception:
  927. pyautogui.write(message, interval=0.03)
  928. else:
  929. pyautogui.write(message, interval=0.03)
  930. time.sleep(0.3)
  931. pyautogui.press("enter")
  932. logger.info("✅ 消息已发送到 Cursor Chat")
  933. return True
  934. def send_chat_for_tasks(force: bool = False, use_agent: bool = True) -> bool:
  935. """
  936. 向 Cursor Chat 发送任务提醒
  937. Args:
  938. force: 强制发送,忽略 ENABLE_CHAT 设置
  939. use_agent: 是否使用新 Agent 模式(推荐)
  940. Returns:
  941. 是否成功发送
  942. """
  943. if not force and not ENABLE_CHAT:
  944. return False
  945. if not HAS_CURSOR_GUI:
  946. logger.warning("未安装 GUI 自动化依赖,无法发送 Chat 消息")
  947. return False
  948. processing_ids = get_processing_task_ids()
  949. if not processing_ids:
  950. logger.info("没有 processing 任务,跳过发送")
  951. return False
  952. logger.info(f"📤 发送任务提醒({len(processing_ids)} 个任务)...")
  953. if use_agent:
  954. # 使用新的 Agent 模式
  955. return run_agent_session(
  956. message=CHAT_MESSAGE,
  957. wait_completion=False, # 在循环模式中不阻塞等待
  958. auto_close=False, # 保持 Agent 打开,让其完成任务
  959. )
  960. else:
  961. # 传统模式
  962. return send_chat_message(CHAT_MESSAGE, CHAT_INPUT_POS)
  963. def chat_trigger_loop(
  964. interval: int = 60,
  965. use_agent: bool = True,
  966. auto_close_on_complete: bool = True,
  967. ) -> None:
  968. """
  969. 循环模式:定期检查 processing 任务并发送 Chat 消息
  970. Args:
  971. interval: 检查间隔(秒),默认 60 秒
  972. use_agent: 是否使用新 Agent 模式
  973. auto_close_on_complete: 任务完成后是否自动关闭 Agent
  974. """
  975. global AGENT_SESSION_ACTIVE
  976. logger.info("=" * 60)
  977. logger.info("🚀 Cursor Chat 自动触发服务已启动")
  978. logger.info(f"⏰ 检查间隔: {interval} 秒")
  979. logger.info(f"🤖 Agent 模式: {'已启用' if use_agent else '未启用'}")
  980. logger.info(f"📝 发送消息: {CHAT_MESSAGE}")
  981. logger.info("按 Ctrl+C 停止服务")
  982. logger.info("=" * 60)
  983. last_sent_time = 0
  984. min_send_interval = 120 # 最小发送间隔(秒),避免频繁打扰
  985. try:
  986. while True:
  987. try:
  988. # 1. 先同步已完成任务
  989. sync_completed_tasks_to_db()
  990. # 2. 从数据库拉取 pending 任务并转为 processing
  991. pending_tasks = get_pending_tasks()
  992. if pending_tasks:
  993. logger.info(f"📥 从数据库发现 {len(pending_tasks)} 个 pending 任务")
  994. # 更新任务状态为 processing
  995. for task in pending_tasks:
  996. update_task_status(task["task_id"], "processing")
  997. # 写入 pending_tasks.json
  998. write_pending_tasks_json(pending_tasks)
  999. # 生成执行指令文件
  1000. create_execute_instructions(pending_tasks)
  1001. logger.info("✅ 已将 pending 任务转为 processing")
  1002. # 3. 检查是否有 processing 任务
  1003. processing_ids = get_processing_task_ids()
  1004. if processing_ids:
  1005. current_time = time.time()
  1006. time_since_last = current_time - last_sent_time
  1007. # 如果有活动的 Agent 会话,不需要重新发送
  1008. if AGENT_SESSION_ACTIVE:
  1009. logger.info(
  1010. f"🤖 Agent 正在执行中,剩余 {len(processing_ids)} 个任务"
  1011. )
  1012. elif time_since_last >= min_send_interval:
  1013. logger.info(f"📋 发现 {len(processing_ids)} 个待处理任务")
  1014. # 更新触发器文件
  1015. update_trigger_file(
  1016. task_count=len(processing_ids),
  1017. status="有待执行任务",
  1018. task_ids=processing_ids,
  1019. )
  1020. # 发送 Chat 消息(启动 Agent)
  1021. if use_agent:
  1022. if open_new_agent():
  1023. if type_message_to_agent(CHAT_MESSAGE):
  1024. last_sent_time = current_time
  1025. logger.info("✅ 已启动 Agent 并发送执行提醒")
  1026. else:
  1027. logger.warning("⚠️ 发送消息失败")
  1028. close_current_agent()
  1029. else:
  1030. logger.warning("⚠️ 启动 Agent 失败")
  1031. else:
  1032. if send_chat_message(CHAT_MESSAGE, CHAT_INPUT_POS):
  1033. last_sent_time = current_time
  1034. logger.info("✅ 已发送执行提醒")
  1035. else:
  1036. logger.warning("⚠️ 发送失败,将在下次重试")
  1037. else:
  1038. remaining = int(min_send_interval - time_since_last)
  1039. logger.info(
  1040. f"⏳ 距离上次发送不足 {min_send_interval}s,"
  1041. f"还需等待 {remaining}s"
  1042. )
  1043. else:
  1044. # 没有待处理任务
  1045. if AGENT_SESSION_ACTIVE and auto_close_on_complete:
  1046. logger.info("✅ 所有任务已完成,正在关闭 Agent...")
  1047. close_current_agent()
  1048. logger.info("✅ Agent 已关闭")
  1049. else:
  1050. logger.info("✅ 没有待处理任务")
  1051. logger.info(f"⏳ {interval} 秒后再次检查...")
  1052. time.sleep(interval)
  1053. except KeyboardInterrupt:
  1054. raise
  1055. except Exception as e:
  1056. logger.error(f"❌ 执行出错: {e}")
  1057. time.sleep(interval)
  1058. except KeyboardInterrupt:
  1059. # 退出时关闭 Agent
  1060. if AGENT_SESSION_ACTIVE:
  1061. logger.info("正在关闭 Agent...")
  1062. close_current_agent()
  1063. logger.info("\n⛔ 服务已停止")
  1064. # ============================================================================
  1065. # 主执行流程
  1066. # ============================================================================
  1067. def auto_execute_tasks_once() -> int:
  1068. """执行一次任务检查和处理"""
  1069. # 1. 先同步已完成任务到数据库
  1070. sync_completed_tasks_to_db()
  1071. # 2. 获取 pending 任务
  1072. logger.info("🔍 检查 pending 任务...")
  1073. tasks = get_pending_tasks()
  1074. # 3. 检查是否有现有的 processing 任务
  1075. existing_processing_ids = get_processing_task_ids()
  1076. if not tasks and not existing_processing_ids:
  1077. logger.info("✅ 没有 pending 或 processing 任务")
  1078. # 更新触发器文件为"已完成"状态
  1079. update_trigger_file(0, "所有任务已完成", [])
  1080. return 0
  1081. if tasks:
  1082. logger.info(f"📋 找到 {len(tasks)} 个 pending 任务")
  1083. # 4. 更新任务状态为 processing
  1084. for task in tasks:
  1085. update_task_status(task["task_id"], "processing")
  1086. # 5. 写入 pending_tasks.json
  1087. write_pending_tasks_json(tasks)
  1088. # 6. 生成执行指令文件
  1089. create_execute_instructions(tasks)
  1090. # 7. 更新触发器文件(关键:触发 Cursor 自动执行)
  1091. all_processing_ids = get_processing_task_ids()
  1092. if all_processing_ids:
  1093. update_trigger_file(
  1094. task_count=len(all_processing_ids),
  1095. status="有待执行任务",
  1096. task_ids=all_processing_ids,
  1097. )
  1098. logger.info(
  1099. f"🔔 已更新触发器,等待 Cursor 执行 {len(all_processing_ids)} 个任务"
  1100. )
  1101. return len(tasks)
  1102. def auto_execute_tasks_loop(interval: int = 300) -> None:
  1103. """循环执行任务检查"""
  1104. logger.info("=" * 60)
  1105. logger.info("🚀 自动任务执行服务已启动")
  1106. logger.info(f"⏰ 检查间隔: {interval} 秒")
  1107. logger.info(f"💬 自动 Chat: {'已启用' if ENABLE_CHAT else '未启用'}")
  1108. logger.info("按 Ctrl+C 停止服务")
  1109. logger.info("=" * 60)
  1110. try:
  1111. while True:
  1112. try:
  1113. count = auto_execute_tasks_once()
  1114. if count > 0:
  1115. send_chat_for_tasks()
  1116. logger.info(f"✅ 已处理 {count} 个任务")
  1117. logger.info(f"⏳ {interval} 秒后再次检查...")
  1118. time.sleep(interval)
  1119. except KeyboardInterrupt:
  1120. raise
  1121. except Exception as e:
  1122. logger.error(f"❌ 执行出错: {e}")
  1123. time.sleep(interval)
  1124. except KeyboardInterrupt:
  1125. logger.info("\n⛔ 服务已停止")
  1126. def refresh_trigger_only() -> None:
  1127. """仅刷新触发器文件(用于现有 processing 任务)"""
  1128. processing_ids = get_processing_task_ids()
  1129. if not processing_ids:
  1130. logger.info("没有 processing 状态的任务")
  1131. update_trigger_file(0, "所有任务已完成", [])
  1132. return
  1133. logger.info(f"📋 发现 {len(processing_ids)} 个 processing 任务")
  1134. # 重新生成指令文件(从 pending_tasks.json 读取)
  1135. if PENDING_TASKS_FILE.exists():
  1136. try:
  1137. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  1138. all_tasks = json.load(f)
  1139. processing_tasks = [t for t in all_tasks if t.get("status") == "processing"]
  1140. if processing_tasks:
  1141. # 重新生成执行指令文件
  1142. create_execute_instructions_from_json(processing_tasks)
  1143. except Exception as e:
  1144. logger.error(f"读取任务文件失败: {e}")
  1145. # 更新触发器文件
  1146. update_trigger_file(
  1147. task_count=len(processing_ids),
  1148. status="有待执行任务",
  1149. task_ids=processing_ids,
  1150. )
  1151. logger.info("✅ 触发器已刷新,等待 Cursor 执行任务")
  1152. def create_execute_instructions_from_json(tasks: list[dict[str, Any]]) -> None:
  1153. """从 pending_tasks.json 格式的任务列表生成执行指令文件"""
  1154. TASKS_DIR.mkdir(parents=True, exist_ok=True)
  1155. with INSTRUCTIONS_FILE.open("w", encoding="utf-8") as f:
  1156. f.write("# 🤖 Cursor 自动任务执行指令\n\n")
  1157. f.write("**⚠️ 重要:请立即执行以下任务!**\n\n")
  1158. gen_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  1159. f.write(f"**生成时间**: {gen_time}\n\n")
  1160. f.write(f"**待执行任务数量**: {len(tasks)}\n\n")
  1161. f.write("## 📋 任务完成后的操作\n\n")
  1162. f.write("完成每个任务后,请更新 `tasks/pending_tasks.json` 中")
  1163. f.write("对应任务的 `status` 为 `completed`,\n")
  1164. f.write("并填写 `code_name`(代码文件名)和 `code_path`(代码路径)。\n\n")
  1165. f.write("调度脚本会自动将完成的任务同步到数据库。\n\n")
  1166. f.write("## ⚠️ 任务约束要求\n\n")
  1167. f.write("**重要约束**:完成脚本创建后,**不需要生成任务总结文件**。\n\n")
  1168. f.write("- ❌ 不要创建任何 summary、report、总结类的文档文件\n")
  1169. f.write("- ❌ 不要生成 task_summary.md、execution_report.md 等总结文件\n")
  1170. f.write("- ✅ 只需创建任务要求的功能脚本文件\n")
  1171. f.write("- ✅ 只需更新 `tasks/pending_tasks.json` 中的任务状态\n\n")
  1172. f.write("---\n\n")
  1173. for idx, task in enumerate(tasks, 1):
  1174. task_id = task.get("task_id", "N/A")
  1175. task_name = task.get("task_name", "未命名任务")
  1176. task_desc = task.get("task_description", "无描述")
  1177. notified_at = task.get("notified_at", "")
  1178. f.write(f"## 🔴 任务 {idx}: {task_name}\n\n")
  1179. f.write(f"- **任务ID**: `{task_id}`\n")
  1180. f.write(f"- **通知时间**: {notified_at}\n")
  1181. f.write(f"- **代码路径**: {task.get('code_path', 'N/A')}\n\n")
  1182. f.write(f"### 📝 任务描述\n\n{task_desc}\n\n")
  1183. f.write("---\n\n")
  1184. logger.info(f"✅ 执行指令文件已更新: {INSTRUCTIONS_FILE}")
  1185. def main() -> None:
  1186. """主函数"""
  1187. parser = argparse.ArgumentParser(
  1188. description="自动任务执行调度脚本",
  1189. formatter_class=argparse.RawDescriptionHelpFormatter,
  1190. epilog="""
  1191. 示例:
  1192. # 执行一次任务检查
  1193. python scripts/auto_execute_tasks.py --once
  1194. # 使用 Agent 模式执行任务(自动部署)
  1195. python scripts/auto_execute_tasks.py --agent-run
  1196. # 启动 Agent 循环模式(推荐,自动部署)
  1197. python scripts/auto_execute_tasks.py --chat-loop --use-agent
  1198. # 禁用自动部署功能
  1199. python scripts/auto_execute_tasks.py --chat-loop --use-agent --no-deploy
  1200. # 立即部署指定任务到生产服务器
  1201. python scripts/auto_execute_tasks.py --deploy-now 123
  1202. # 测试生产服务器连接
  1203. python scripts/auto_execute_tasks.py --test-connection
  1204. """,
  1205. )
  1206. parser.add_argument("--once", action="store_true", help="只执行一次")
  1207. parser.add_argument("--interval", type=int, default=300, help="检查间隔(秒)")
  1208. parser.add_argument(
  1209. "--enable-chat", action="store_true", help="启用自动 Cursor Chat"
  1210. )
  1211. parser.add_argument("--chat-input-pos", type=str, help='Chat 输入框位置 "x,y"')
  1212. parser.add_argument(
  1213. "--chat-message",
  1214. type=str,
  1215. default="请阅读 tasks/task_execute_instructions.md 并执行任务。",
  1216. help="发送到 Chat 的消息",
  1217. )
  1218. parser.add_argument(
  1219. "--refresh-trigger",
  1220. action="store_true",
  1221. help="仅刷新触发器文件(用于现有 processing 任务)",
  1222. )
  1223. parser.add_argument(
  1224. "--chat-loop",
  1225. action="store_true",
  1226. help="启动 Chat 自动触发循环(定期发送执行消息)",
  1227. )
  1228. parser.add_argument(
  1229. "--chat-interval",
  1230. type=int,
  1231. default=60,
  1232. help="Chat 触发循环的检查间隔(秒),默认 60",
  1233. )
  1234. parser.add_argument(
  1235. "--send-chat-now",
  1236. action="store_true",
  1237. help="立即发送一次 Chat 消息(用于手动触发)",
  1238. )
  1239. # Agent 模式相关参数
  1240. parser.add_argument(
  1241. "--use-agent",
  1242. action="store_true",
  1243. default=True,
  1244. help="使用新 Agent 模式(默认启用)",
  1245. )
  1246. parser.add_argument(
  1247. "--no-agent",
  1248. action="store_true",
  1249. help="禁用 Agent 模式,使用传统 Chat 方式",
  1250. )
  1251. parser.add_argument(
  1252. "--agent-run",
  1253. action="store_true",
  1254. help="立即启动 Agent 会话执行任务",
  1255. )
  1256. parser.add_argument(
  1257. "--agent-timeout",
  1258. type=int,
  1259. default=3600,
  1260. help="Agent 等待任务完成的超时时间(秒),默认 3600",
  1261. )
  1262. parser.add_argument(
  1263. "--no-auto-close",
  1264. action="store_true",
  1265. help="任务完成后不自动关闭 Agent",
  1266. )
  1267. # 自动部署相关参数
  1268. parser.add_argument(
  1269. "--enable-deploy",
  1270. action="store_true",
  1271. default=True,
  1272. help="启用自动部署到生产服务器(默认启用)",
  1273. )
  1274. parser.add_argument(
  1275. "--no-deploy",
  1276. action="store_true",
  1277. help="禁用自动部署功能",
  1278. )
  1279. parser.add_argument(
  1280. "--deploy-now",
  1281. type=str,
  1282. metavar="TASK_ID",
  1283. help="立即部署指定任务ID的脚本到生产服务器",
  1284. )
  1285. parser.add_argument(
  1286. "--test-connection",
  1287. action="store_true",
  1288. help="测试到生产服务器的 SSH 连接",
  1289. )
  1290. args = parser.parse_args()
  1291. global ENABLE_CHAT, CHAT_INPUT_POS, CHAT_MESSAGE, ENABLE_AUTO_DEPLOY
  1292. ENABLE_CHAT = bool(args.enable_chat)
  1293. CHAT_MESSAGE = args.chat_message
  1294. ENABLE_AUTO_DEPLOY = args.enable_deploy and not args.no_deploy
  1295. # 确定是否使用 Agent 模式
  1296. use_agent = args.use_agent and not args.no_agent
  1297. auto_close = not args.no_auto_close
  1298. if args.chat_input_pos:
  1299. try:
  1300. x, y = args.chat_input_pos.split(",")
  1301. CHAT_INPUT_POS = (int(x.strip()), int(y.strip()))
  1302. except Exception:
  1303. pass
  1304. # 测试 SSH 连接
  1305. if args.test_connection:
  1306. if test_ssh_connection():
  1307. logger.info("✅ 连接测试成功")
  1308. else:
  1309. logger.error("❌ 连接测试失败")
  1310. return
  1311. # 立即部署指定任务
  1312. if args.deploy_now:
  1313. try:
  1314. task_id = int(args.deploy_now)
  1315. logger.info(f"🚀 开始部署任务 {task_id}...")
  1316. # 从 pending_tasks.json 查找任务信息
  1317. if PENDING_TASKS_FILE.exists():
  1318. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  1319. tasks = json.load(f)
  1320. task_found = None
  1321. for t in tasks:
  1322. if t.get("task_id") == task_id:
  1323. task_found = t
  1324. break
  1325. if task_found:
  1326. if auto_deploy_completed_task(task_found):
  1327. logger.info(f"✅ 任务 {task_id} 部署成功")
  1328. else:
  1329. logger.error(f"❌ 任务 {task_id} 部署失败")
  1330. else:
  1331. logger.error(f"未找到任务 {task_id}")
  1332. else:
  1333. logger.error("pending_tasks.json 文件不存在")
  1334. except ValueError:
  1335. logger.error(f"无效的任务ID: {args.deploy_now}")
  1336. return
  1337. # 仅刷新触发器
  1338. if args.refresh_trigger:
  1339. refresh_trigger_only()
  1340. return
  1341. # 立即启动 Agent 会话
  1342. if args.agent_run:
  1343. processing_ids = get_processing_task_ids()
  1344. if not processing_ids:
  1345. # 先检查是否有 pending 任务
  1346. auto_execute_tasks_once()
  1347. processing_ids = get_processing_task_ids()
  1348. if not processing_ids:
  1349. logger.info("没有待执行的任务")
  1350. return
  1351. logger.info(f"🚀 启动 Agent 执行 {len(processing_ids)} 个任务...")
  1352. success = run_agent_session(
  1353. message=CHAT_MESSAGE,
  1354. wait_completion=True,
  1355. timeout=args.agent_timeout,
  1356. auto_close=auto_close,
  1357. )
  1358. if success:
  1359. logger.info("✅ Agent 会话完成")
  1360. else:
  1361. logger.error("❌ Agent 会话失败")
  1362. return
  1363. # 立即发送一次 Chat 消息
  1364. if args.send_chat_now:
  1365. if use_agent:
  1366. # 使用 Agent 模式
  1367. processing_ids = get_processing_task_ids()
  1368. if not processing_ids:
  1369. auto_execute_tasks_once()
  1370. processing_ids = get_processing_task_ids()
  1371. if processing_ids:
  1372. if open_new_agent():
  1373. if type_message_to_agent(CHAT_MESSAGE):
  1374. logger.info("✅ Agent 已启动并发送消息")
  1375. else:
  1376. logger.error("❌ 发送消息失败")
  1377. else:
  1378. logger.error("❌ 启动 Agent 失败")
  1379. else:
  1380. logger.info("没有待执行的任务")
  1381. else:
  1382. if send_chat_for_tasks(force=True, use_agent=False):
  1383. logger.info("✅ 消息已发送")
  1384. else:
  1385. logger.error("❌ 发送失败")
  1386. return
  1387. # Chat 自动触发循环模式
  1388. if args.chat_loop:
  1389. chat_trigger_loop(
  1390. interval=args.chat_interval,
  1391. use_agent=use_agent,
  1392. auto_close_on_complete=auto_close,
  1393. )
  1394. return
  1395. if args.once:
  1396. count = auto_execute_tasks_once()
  1397. if count > 0:
  1398. send_chat_for_tasks(use_agent=use_agent)
  1399. logger.info(f"✅ 完成!处理了 {count} 个任务")
  1400. else:
  1401. auto_execute_tasks_loop(interval=args.interval)
  1402. if __name__ == "__main__":
  1403. main()