auto_execute_tasks.py 88 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577
  1. #!/usr/bin/env python3
  2. """
  3. 自动任务执行核心调度脚本 (Agent 模式)
  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. # Agent 单次执行(执行一次任务后退出)
  14. python scripts/auto_execute_tasks.py --agent-run
  15. # Agent 循环模式(有任务时自动启动 Agent,完成后等待新任务)
  16. python scripts/auto_execute_tasks.py --agent-loop
  17. # Agent 循环模式 + 禁用自动部署
  18. python scripts/auto_execute_tasks.py --agent-loop --no-deploy
  19. # 设置 Agent 超时时间(默认 3600 秒)
  20. python scripts/auto_execute_tasks.py --agent-run --agent-timeout 7200
  21. # 任务完成后不自动关闭 Agent
  22. python scripts/auto_execute_tasks.py --agent-run --no-auto-close
  23. # 立即部署指定任务ID的脚本到生产服务器
  24. python scripts/auto_execute_tasks.py --deploy-now 123
  25. # 测试到生产服务器的 SSH 连接
  26. python scripts/auto_execute_tasks.py --test-connection
  27. """
  28. from __future__ import annotations
  29. import argparse
  30. import contextlib
  31. import json
  32. import logging
  33. import sys
  34. import time
  35. from datetime import datetime
  36. from pathlib import Path
  37. from typing import Any
  38. # ============================================================================
  39. # 日志配置
  40. # ============================================================================
  41. logging.basicConfig(
  42. level=logging.INFO,
  43. format="%(asctime)s - %(levelname)s - %(message)s",
  44. )
  45. logger = logging.getLogger("AutoExecuteTasks")
  46. # ============================================================================
  47. # Windows GUI 自动化依赖(可选)
  48. # ============================================================================
  49. HAS_CURSOR_GUI = False
  50. HAS_PYPERCLIP = False
  51. try:
  52. import pyautogui
  53. import win32con
  54. import win32gui
  55. pyautogui.FAILSAFE = True
  56. pyautogui.PAUSE = 0.5
  57. HAS_CURSOR_GUI = True
  58. try:
  59. import pyperclip
  60. HAS_PYPERCLIP = True
  61. except ImportError:
  62. pass
  63. except ImportError:
  64. logger.info(
  65. "未安装 Windows GUI 自动化依赖(pywin32/pyautogui),"
  66. "将禁用自动 Cursor Agent 功能。"
  67. )
  68. # ============================================================================
  69. # 全局配置
  70. # ============================================================================
  71. WORKSPACE_ROOT = Path(__file__).parent.parent
  72. TASKS_DIR = WORKSPACE_ROOT / "tasks"
  73. PENDING_TASKS_FILE = TASKS_DIR / "pending_tasks.json"
  74. INSTRUCTIONS_FILE = TASKS_DIR / "task_execute_instructions.md"
  75. TRIGGER_FILE = TASKS_DIR / "task_trigger.txt"
  76. # 生产服务器配置
  77. PRODUCTION_SERVER = {
  78. "host": "192.168.3.143",
  79. "port": 22,
  80. "username": "ubuntu",
  81. "password": "citumxl2357",
  82. "script_path": "/opt/dataops-platform/datafactory/scripts",
  83. "workflow_path": "/opt/dataops-platform/datafactory/workflows", # 工作流 JSON 文件目录
  84. }
  85. # Agent 消息模板
  86. AGENT_MESSAGE = "请阅读 tasks/task_execute_instructions.md 并执行任务。"
  87. # 命令行参数控制的全局变量
  88. ENABLE_AUTO_DEPLOY: bool = True # 默认启用自动部署
  89. # ============================================================================
  90. # 数据库操作
  91. # ============================================================================
  92. def get_db_connection():
  93. """获取数据库连接(使用 production 环境配置)"""
  94. try:
  95. from urllib.parse import urlparse
  96. import psycopg2
  97. sys.path.insert(0, str(WORKSPACE_ROOT))
  98. from app.config.config import config
  99. # 强制使用 production 环境的数据库配置
  100. app_config = config["production"]
  101. db_uri = app_config.SQLALCHEMY_DATABASE_URI
  102. # 解析 SQLAlchemy URI 格式为 psycopg2 可用的格式
  103. parsed = urlparse(db_uri)
  104. conn = psycopg2.connect(
  105. host=parsed.hostname,
  106. port=parsed.port or 5432,
  107. database=parsed.path.lstrip("/"),
  108. user=parsed.username,
  109. password=parsed.password,
  110. )
  111. logger.debug(
  112. f"数据库连接成功: {parsed.hostname}:{parsed.port}/{parsed.path.lstrip('/')}"
  113. )
  114. return conn
  115. except ImportError as e:
  116. logger.error(f"导入依赖失败: {e}")
  117. return None
  118. except Exception as e:
  119. logger.error(f"连接数据库失败: {e}")
  120. import traceback
  121. logger.error(traceback.format_exc())
  122. return None
  123. def get_pending_tasks() -> list[dict[str, Any]]:
  124. """
  125. 从 PostgreSQL task_list 表获取所有 pending 状态的任务
  126. 重要:此函数直接查询数据库,确保获取最新的任务列表
  127. """
  128. try:
  129. from psycopg2.extras import RealDictCursor
  130. logger.info("📡 正在连接数据库...")
  131. conn = get_db_connection()
  132. if not conn:
  133. logger.error("❌ 无法获取数据库连接")
  134. return []
  135. logger.info("✅ 数据库连接成功,正在查询 pending 任务...")
  136. cursor = conn.cursor(cursor_factory=RealDictCursor)
  137. cursor.execute(
  138. """
  139. SELECT task_id, task_name, task_description, status,
  140. code_name, code_path, create_time, create_by
  141. FROM task_list
  142. WHERE status = 'pending'
  143. ORDER BY create_time ASC
  144. """
  145. )
  146. tasks = cursor.fetchall()
  147. cursor.close()
  148. conn.close()
  149. task_list = [dict(task) for task in tasks]
  150. logger.info(f"📊 从数据库查询到 {len(task_list)} 个 pending 任务")
  151. if task_list:
  152. for task in task_list:
  153. logger.info(f" - 任务 {task['task_id']}: {task['task_name']}")
  154. return task_list
  155. except Exception as e:
  156. logger.error(f"获取 pending 任务失败: {e}")
  157. import traceback
  158. logger.error(traceback.format_exc())
  159. return []
  160. def update_task_status(
  161. task_id: int,
  162. status: str,
  163. code_name: str | None = None,
  164. code_path: str | None = None,
  165. ) -> bool:
  166. """更新任务状态"""
  167. try:
  168. conn = get_db_connection()
  169. if not conn:
  170. return False
  171. cursor = conn.cursor()
  172. if code_name and code_path:
  173. cursor.execute(
  174. """
  175. UPDATE task_list
  176. SET status = %s, code_name = %s, code_path = %s,
  177. update_time = CURRENT_TIMESTAMP
  178. WHERE task_id = %s
  179. """,
  180. (status, code_name, code_path, task_id),
  181. )
  182. else:
  183. cursor.execute(
  184. """
  185. UPDATE task_list
  186. SET status = %s, update_time = CURRENT_TIMESTAMP
  187. WHERE task_id = %s
  188. """,
  189. (status, task_id),
  190. )
  191. conn.commit()
  192. updated = cursor.rowcount > 0
  193. cursor.close()
  194. conn.close()
  195. if updated:
  196. logger.info(f"✅ 任务 {task_id} 状态已更新为: {status}")
  197. return updated
  198. except Exception as e:
  199. logger.error(f"更新任务状态失败: {e}")
  200. return False
  201. # ============================================================================
  202. # 触发器文件管理
  203. # ============================================================================
  204. def update_trigger_file(
  205. task_count: int = 0,
  206. status: str = "有待执行任务",
  207. task_ids: list[int] | None = None,
  208. ) -> None:
  209. """更新触发器文件 tasks/task_trigger.txt"""
  210. TASKS_DIR.mkdir(parents=True, exist_ok=True)
  211. gen_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  212. task_ids_str = ", ".join(str(tid) for tid in (task_ids or []))
  213. content = f"""CURSOR_AUTO_EXECUTE_TASK_TRIGGER
  214. 生成时间: {gen_time}
  215. 状态: {status}
  216. 待处理任务数: {task_count}
  217. 任务ID列表: [{task_ids_str}]
  218. 此文件用于触发Cursor自动执行任务。
  219. 【重要】Cursor AI 请立即执行以下操作:
  220. 1. 读取 tasks/task_execute_instructions.md 文件
  221. 2. 按照文件中的任务描述,依次完成每个任务
  222. 3. 完成后更新 tasks/pending_tasks.json 中对应任务的状态为 completed
  223. """
  224. with TRIGGER_FILE.open("w", encoding="utf-8") as f:
  225. f.write(content)
  226. logger.info(f"✅ 触发器文件已更新: {TRIGGER_FILE}")
  227. def get_processing_task_ids() -> list[int]:
  228. """从 pending_tasks.json 获取所有 processing 状态的任务 ID"""
  229. if not PENDING_TASKS_FILE.exists():
  230. return []
  231. try:
  232. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  233. tasks = json.load(f)
  234. return [
  235. t.get("task_id")
  236. for t in tasks
  237. if t.get("status") == "processing" and t.get("task_id")
  238. ]
  239. except Exception:
  240. return []
  241. def get_tasks_by_ids(task_ids: list[int]) -> list[dict[str, Any]]:
  242. """
  243. 根据任务 ID 列表从数据库获取任务详细信息
  244. Args:
  245. task_ids: 任务 ID 列表
  246. Returns:
  247. 包含任务详细信息的列表(包括 task_description)
  248. """
  249. if not task_ids:
  250. return []
  251. try:
  252. from psycopg2.extras import RealDictCursor
  253. conn = get_db_connection()
  254. if not conn:
  255. logger.error("无法获取数据库连接")
  256. return []
  257. cursor = conn.cursor(cursor_factory=RealDictCursor)
  258. # 构建 IN 查询
  259. placeholders = ", ".join(["%s"] * len(task_ids))
  260. query = f"""
  261. SELECT task_id, task_name, task_description, status,
  262. code_name, code_path, create_time, create_by
  263. FROM task_list
  264. WHERE task_id IN ({placeholders})
  265. ORDER BY create_time ASC
  266. """
  267. cursor.execute(query, tuple(task_ids))
  268. tasks = cursor.fetchall()
  269. cursor.close()
  270. conn.close()
  271. task_list = [dict(task) for task in tasks]
  272. logger.info(f"从数据库获取了 {len(task_list)} 个任务的详细信息")
  273. return task_list
  274. except Exception as e:
  275. logger.error(f"根据 ID 获取任务失败: {e}")
  276. import traceback
  277. logger.error(traceback.format_exc())
  278. return []
  279. def get_all_tasks_to_execute() -> list[dict[str, Any]]:
  280. """
  281. 获取所有需要执行的任务(包括新的 pending 任务和已有的 processing 任务)
  282. 此函数确保返回的任务列表包含完整信息(特别是 task_description),
  283. 用于生成执行指令文件。
  284. Returns:
  285. 包含所有需要执行任务的完整信息列表
  286. """
  287. # 1. 获取本地 pending_tasks.json 中 processing 状态的任务 ID
  288. processing_ids = get_processing_task_ids()
  289. # 2. 从数据库获取所有 pending 任务
  290. pending_tasks = get_pending_tasks()
  291. pending_ids = [t["task_id"] for t in pending_tasks]
  292. # 3. 合并所有需要查询的任务 ID(去重)
  293. all_task_ids = list(set(processing_ids + pending_ids))
  294. if not all_task_ids:
  295. return []
  296. # 4. 从数据库获取这些任务的完整信息
  297. all_tasks = get_tasks_by_ids(all_task_ids)
  298. logger.info(
  299. f"需要执行的任务: {len(all_tasks)} 个 "
  300. f"(processing: {len(processing_ids)}, pending: {len(pending_ids)})"
  301. )
  302. return all_tasks
  303. # ============================================================================
  304. # 任务文件生成
  305. # ============================================================================
  306. def write_pending_tasks_json(tasks: list[dict[str, Any]]) -> None:
  307. """将任务列表写入 tasks/pending_tasks.json"""
  308. TASKS_DIR.mkdir(parents=True, exist_ok=True)
  309. # 读取现有任务
  310. existing_tasks = []
  311. if PENDING_TASKS_FILE.exists():
  312. try:
  313. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  314. existing_tasks = json.load(f)
  315. except Exception:
  316. existing_tasks = []
  317. existing_ids = {t["task_id"] for t in existing_tasks if "task_id" in t}
  318. # 添加新任务
  319. for task in tasks:
  320. if task["task_id"] not in existing_ids:
  321. task_info = {
  322. "task_id": task["task_id"],
  323. "task_name": task["task_name"],
  324. "code_path": task.get("code_path", ""),
  325. "code_name": task.get("code_name", ""),
  326. "status": "processing",
  327. "notified_at": datetime.now().isoformat(),
  328. "code_file": task.get("code_file", ""),
  329. }
  330. existing_tasks.append(task_info)
  331. with PENDING_TASKS_FILE.open("w", encoding="utf-8") as f:
  332. json.dump(existing_tasks, f, indent=2, ensure_ascii=False)
  333. logger.info(f"✅ pending_tasks.json 已更新,任务数: {len(existing_tasks)}")
  334. def create_execute_instructions(tasks: list[dict[str, Any]]) -> None:
  335. """生成任务执行指令文件 tasks/task_execute_instructions.md"""
  336. TASKS_DIR.mkdir(parents=True, exist_ok=True)
  337. with INSTRUCTIONS_FILE.open("w", encoding="utf-8") as f:
  338. f.write("# Cursor 自动任务执行指令\n\n")
  339. f.write("**重要:请立即执行以下任务!**\n\n")
  340. gen_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  341. f.write(f"**生成时间**: {gen_time}\n\n")
  342. f.write(f"**待执行任务数量**: {len(tasks)}\n\n")
  343. f.write("## 任务完成后的操作\n\n")
  344. f.write("完成每个任务后,请更新 `tasks/pending_tasks.json` 中")
  345. f.write("对应任务的 `status` 为 `completed`,\n")
  346. f.write("并填写 `code_name`(代码文件名)和 `code_path`(代码路径)。\n\n")
  347. f.write("调度脚本会自动将完成的任务同步到数据库。\n\n")
  348. f.write("## 任务约束要求\n\n")
  349. f.write("**重要约束**:完成脚本创建后,**不需要生成任务总结文件**。\n\n")
  350. f.write("- 不要创建任何 summary、report、总结类的文档文件\n")
  351. f.write("- 不要生成 task_summary.md、execution_report.md 等总结文件\n")
  352. f.write("- 只需创建任务要求的功能脚本文件\n")
  353. f.write("- 只需更新 `tasks/pending_tasks.json` 中的任务状态\n\n")
  354. f.write("---\n\n")
  355. for idx, task in enumerate(tasks, 1):
  356. task_id = task["task_id"]
  357. task_name = task["task_name"]
  358. task_desc = task["task_description"]
  359. create_time = task.get("create_time", "")
  360. if hasattr(create_time, "strftime"):
  361. create_time = create_time.strftime("%Y-%m-%d %H:%M:%S")
  362. f.write(f"## 任务 {idx}: {task_name}\n\n")
  363. f.write(f"- **任务ID**: `{task_id}`\n")
  364. f.write(f"- **创建时间**: {create_time}\n")
  365. f.write(f"- **创建者**: {task.get('create_by', 'unknown')}\n\n")
  366. f.write(f"### 任务描述\n\n{task_desc}\n\n")
  367. f.write("---\n\n")
  368. logger.info(f"✅ 执行指令文件已创建: {INSTRUCTIONS_FILE}")
  369. # ============================================================================
  370. # Neo4j 独立连接(不依赖 Flask 应用上下文)
  371. # ============================================================================
  372. def get_neo4j_driver():
  373. """获取 Neo4j 驱动(独立于 Flask 应用上下文)"""
  374. try:
  375. from neo4j import GraphDatabase
  376. sys.path.insert(0, str(WORKSPACE_ROOT))
  377. from app.config.config import config
  378. # 强制使用 production 环境的配置
  379. app_config = config["production"]
  380. uri = app_config.NEO4J_URI
  381. user = app_config.NEO4J_USER
  382. password = app_config.NEO4J_PASSWORD
  383. driver = GraphDatabase.driver(uri, auth=(user, password))
  384. return driver
  385. except ImportError as e:
  386. logger.error(f"导入 Neo4j 驱动失败: {e}")
  387. return None
  388. except Exception as e:
  389. logger.error(f"连接 Neo4j 失败: {e}")
  390. return None
  391. # ============================================================================
  392. # 状态同步
  393. # ============================================================================
  394. def extract_dataflow_name_from_task(task_id: int) -> str | None:
  395. """从任务描述中提取 DataFlow 名称"""
  396. import re
  397. try:
  398. conn = get_db_connection()
  399. if not conn:
  400. return None
  401. cursor = conn.cursor()
  402. cursor.execute(
  403. "SELECT task_description FROM task_list WHERE task_id = %s",
  404. (task_id,),
  405. )
  406. result = cursor.fetchone()
  407. cursor.close()
  408. conn.close()
  409. if not result:
  410. return None
  411. task_desc = result[0]
  412. # 从任务描述中提取 DataFlow Name
  413. match = re.search(r"\*\*DataFlow Name\*\*:\s*(.+?)(?:\n|$)", task_desc)
  414. if match:
  415. dataflow_name = match.group(1).strip()
  416. logger.info(f"从任务 {task_id} 提取到 DataFlow 名称: {dataflow_name}")
  417. return dataflow_name
  418. return None
  419. except Exception as e:
  420. logger.error(f"提取 DataFlow 名称失败: {e}")
  421. return None
  422. def update_dataflow_script_path(
  423. task_name: str, script_path: str, task_id: int | None = None
  424. ) -> bool:
  425. """更新 DataFlow 节点的 script_path 字段"""
  426. try:
  427. driver = get_neo4j_driver()
  428. if not driver:
  429. logger.error("无法获取 Neo4j 驱动")
  430. return False
  431. # 如果提供了 task_id,尝试从任务描述中提取真正的 DataFlow 名称
  432. dataflow_name = task_name
  433. if task_id:
  434. extracted_name = extract_dataflow_name_from_task(task_id)
  435. if extracted_name:
  436. dataflow_name = extracted_name
  437. logger.info(f"使用从任务描述提取的 DataFlow 名称: {dataflow_name}")
  438. query = """
  439. MATCH (n:DataFlow {name_zh: $name_zh})
  440. SET n.script_path = $script_path, n.updated_at = $updated_at
  441. RETURN n
  442. """
  443. updated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  444. with driver.session() as session:
  445. result = session.run(
  446. query,
  447. name_zh=dataflow_name,
  448. script_path=script_path,
  449. updated_at=updated_at,
  450. ).single()
  451. driver.close()
  452. if result:
  453. logger.info(
  454. f"成功更新 DataFlow 脚本路径: {dataflow_name} -> {script_path}"
  455. )
  456. return True
  457. else:
  458. logger.warning(f"未找到 DataFlow 节点: {dataflow_name}")
  459. return False
  460. except Exception as e:
  461. logger.error(f"更新 DataFlow script_path 失败: {e}")
  462. return False
  463. def sync_completed_tasks_to_db() -> int:
  464. """将 pending_tasks.json 中 completed 的任务同步到数据库"""
  465. if not PENDING_TASKS_FILE.exists():
  466. return 0
  467. try:
  468. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  469. tasks = json.load(f)
  470. except Exception as e:
  471. logger.error(f"读取 pending_tasks.json 失败: {e}")
  472. return 0
  473. if not isinstance(tasks, list):
  474. return 0
  475. updated = 0
  476. remaining_tasks = []
  477. for t in tasks:
  478. if t.get("status") == "completed":
  479. task_id = t.get("task_id")
  480. if not task_id:
  481. continue
  482. task_name = t.get("task_name")
  483. code_path = t.get("code_path")
  484. # 使用 code_file 字段获取实际的脚本文件名
  485. code_file = t.get("code_file", "")
  486. # 统一处理:code_path 始终为 "datafactory/scripts"
  487. code_path = "datafactory/scripts"
  488. # 使用 code_file 判断是否为 Python 脚本
  489. is_python_script = code_file and code_file.endswith(".py")
  490. if is_python_script:
  491. logger.info(f"任务 {task_id} 使用 Python 脚本: {code_path}/{code_file}")
  492. else:
  493. logger.info(
  494. f"任务 {task_id} 的 code_file ({code_file}) 不是 Python 脚本,跳过 DataFlow 更新"
  495. )
  496. if update_task_status(task_id, "completed", code_file, code_path):
  497. updated += 1
  498. logger.info(f"已同步任务 {task_id} 为 completed")
  499. # 只有 Python 脚本才更新 DataFlow 节点的 script_path
  500. if task_name and is_python_script:
  501. full_script_path = f"{code_path}/{code_file}"
  502. if update_dataflow_script_path(
  503. task_name, full_script_path, task_id=task_id
  504. ):
  505. logger.info(
  506. f"已更新 DataFlow 脚本路径: {task_name} -> {full_script_path}"
  507. )
  508. else:
  509. logger.warning(f"更新 DataFlow 脚本路径失败: {task_name}")
  510. # 自动部署到生产服务器(如果启用)
  511. if ENABLE_AUTO_DEPLOY:
  512. logger.info(f"开始自动部署任务 {task_id} 到生产服务器...")
  513. if auto_deploy_completed_task(t):
  514. logger.info(f"✅ 任务 {task_id} 已成功部署到生产服务器")
  515. else:
  516. logger.warning(f"任务 {task_id} 部署到生产服务器失败")
  517. else:
  518. logger.info(f"自动部署已禁用,跳过任务 {task_id} 的部署")
  519. else:
  520. remaining_tasks.append(t)
  521. else:
  522. remaining_tasks.append(t)
  523. if updated > 0:
  524. with PENDING_TASKS_FILE.open("w", encoding="utf-8") as f:
  525. json.dump(remaining_tasks, f, indent=2, ensure_ascii=False)
  526. logger.info(f"本次共同步 {updated} 个 completed 任务到数据库")
  527. return updated
  528. # ============================================================================
  529. # 生产服务器部署功能
  530. # ============================================================================
  531. def get_ssh_connection():
  532. """获取 SSH 连接到生产服务器"""
  533. try:
  534. import paramiko # type: ignore
  535. ssh = paramiko.SSHClient()
  536. ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  537. logger.info(
  538. f"正在连接生产服务器 {PRODUCTION_SERVER['username']}@"
  539. f"{PRODUCTION_SERVER['host']}:{PRODUCTION_SERVER['port']}..."
  540. )
  541. ssh.connect(
  542. hostname=PRODUCTION_SERVER["host"],
  543. port=PRODUCTION_SERVER["port"],
  544. username=PRODUCTION_SERVER["username"],
  545. password=PRODUCTION_SERVER["password"],
  546. timeout=10,
  547. )
  548. logger.info("✅ SSH 连接成功")
  549. return ssh
  550. except ImportError:
  551. logger.error("未安装 paramiko 库,请运行: pip install paramiko")
  552. return None
  553. except Exception as e:
  554. logger.error(f"SSH 连接失败: {e}")
  555. return None
  556. def test_ssh_connection() -> bool:
  557. """测试 SSH 连接到生产服务器"""
  558. logger.info("=" * 60)
  559. logger.info("测试生产服务器连接")
  560. logger.info("=" * 60)
  561. ssh = get_ssh_connection()
  562. if not ssh:
  563. logger.error("❌ SSH 连接测试失败")
  564. return False
  565. try:
  566. # 测试执行命令
  567. _, stdout, _ = ssh.exec_command("echo 'Connection test successful'")
  568. output = stdout.read().decode().strip()
  569. logger.info(f"✅ 命令执行成功: {output}")
  570. # 检查目标目录是否存在
  571. _, stdout, _ = ssh.exec_command(
  572. f"test -d {PRODUCTION_SERVER['script_path']} && echo 'exists' || echo 'not exists'"
  573. )
  574. result = stdout.read().decode().strip()
  575. if result == "exists":
  576. logger.info(f"✅ 脚本目录存在: {PRODUCTION_SERVER['script_path']}")
  577. else:
  578. logger.warning(f"脚本目录不存在: {PRODUCTION_SERVER['script_path']}")
  579. logger.info("将在首次部署时自动创建")
  580. ssh.close()
  581. logger.info("=" * 60)
  582. logger.info("✅ 连接测试完成")
  583. logger.info("=" * 60)
  584. return True
  585. except Exception as e:
  586. logger.error(f"❌ 测试执行命令失败: {e}")
  587. ssh.close()
  588. return False
  589. def deploy_script_to_production(
  590. local_script_path: str, remote_filename: str | None = None
  591. ) -> bool:
  592. """部署脚本文件到生产服务器"""
  593. try:
  594. import importlib.util
  595. if importlib.util.find_spec("paramiko") is None:
  596. logger.error("未安装 paramiko 库,请运行: pip install paramiko")
  597. return False
  598. # 转换为绝对路径
  599. local_path = Path(local_script_path)
  600. if not local_path.is_absolute():
  601. local_path = WORKSPACE_ROOT / local_path
  602. if not local_path.exists():
  603. logger.error(f"本地文件不存在: {local_path}")
  604. return False
  605. # 确定远程文件名
  606. if not remote_filename:
  607. remote_filename = local_path.name
  608. remote_path = f"{PRODUCTION_SERVER['script_path']}/{remote_filename}"
  609. # 建立 SSH 连接
  610. ssh = get_ssh_connection()
  611. if not ssh:
  612. return False
  613. try:
  614. # 创建 SFTP 客户端
  615. sftp = ssh.open_sftp()
  616. # 确保远程目录存在
  617. try:
  618. sftp.stat(PRODUCTION_SERVER["script_path"])
  619. except FileNotFoundError:
  620. logger.info(f"创建远程目录: {PRODUCTION_SERVER['script_path']}")
  621. _, stdout, _ = ssh.exec_command(
  622. f"mkdir -p {PRODUCTION_SERVER['script_path']}"
  623. )
  624. stdout.channel.recv_exit_status()
  625. # 上传文件
  626. logger.info(f"正在上传: {local_path} -> {remote_path}")
  627. sftp.put(str(local_path), remote_path)
  628. # 设置文件权限为可执行
  629. sftp.chmod(remote_path, 0o755)
  630. logger.info(f"✅ 脚本部署成功: {remote_path}")
  631. sftp.close()
  632. ssh.close()
  633. return True
  634. except Exception as e:
  635. logger.error(f"文件传输失败: {e}")
  636. ssh.close()
  637. return False
  638. except ImportError:
  639. logger.error("未安装 paramiko 库,请运行: pip install paramiko")
  640. return False
  641. except Exception as e:
  642. logger.error(f"部署脚本失败: {e}")
  643. return False
  644. def deploy_n8n_workflow_to_production(workflow_file: str) -> bool:
  645. """
  646. 部署 n8n 工作流到 n8n 服务器
  647. 此函数执行两个步骤:
  648. 1. 通过 n8n API 创建工作流(主要步骤)
  649. 2. 通过 SFTP 备份工作流文件到生产服务器(可选)
  650. """
  651. try:
  652. import json
  653. import requests
  654. # 转换为绝对路径
  655. local_path = Path(workflow_file)
  656. if not local_path.is_absolute():
  657. local_path = WORKSPACE_ROOT / local_path
  658. if not local_path.exists():
  659. logger.error(f"工作流文件不存在: {local_path}")
  660. return False
  661. # 加载工作流 JSON
  662. with open(local_path, encoding="utf-8") as f:
  663. workflow_data = json.load(f)
  664. workflow_name = workflow_data.get("name", local_path.stem)
  665. logger.info(f"正在部署工作流到 n8n 服务器: {workflow_name}")
  666. # 获取 n8n API 配置
  667. try:
  668. sys.path.insert(0, str(WORKSPACE_ROOT))
  669. from app.config.config import BaseConfig
  670. api_url = BaseConfig.N8N_API_URL
  671. api_key = BaseConfig.N8N_API_KEY
  672. timeout = BaseConfig.N8N_API_TIMEOUT
  673. except (ImportError, AttributeError):
  674. import os
  675. api_url = os.environ.get("N8N_API_URL", "https://n8n.citupro.com")
  676. api_key = os.environ.get("N8N_API_KEY", "")
  677. timeout = int(os.environ.get("N8N_API_TIMEOUT", "30"))
  678. if not api_key:
  679. logger.error("未配置 N8N_API_KEY,无法部署工作流到 n8n 服务器")
  680. return False
  681. # 准备 API 请求
  682. headers = {
  683. "X-N8N-API-KEY": api_key,
  684. "Content-Type": "application/json",
  685. "Accept": "application/json",
  686. }
  687. # 准备工作流数据(移除 tags,n8n API 不支持直接创建带 tags)
  688. workflow_payload = {
  689. "name": workflow_name,
  690. "nodes": workflow_data.get("nodes", []),
  691. "connections": workflow_data.get("connections", {}),
  692. "settings": workflow_data.get("settings", {}),
  693. }
  694. # 调用 n8n API 创建工作流
  695. create_url = f"{api_url.rstrip('/')}/api/v1/workflows"
  696. logger.info(f"调用 n8n API: {create_url}")
  697. try:
  698. response = requests.post(
  699. create_url,
  700. headers=headers,
  701. json=workflow_payload,
  702. timeout=timeout,
  703. )
  704. if response.status_code == 401:
  705. logger.error("n8n API 认证失败,请检查 N8N_API_KEY 配置")
  706. return False
  707. elif response.status_code == 403:
  708. logger.error("n8n API 权限不足")
  709. return False
  710. response.raise_for_status()
  711. created_workflow = response.json()
  712. workflow_id = created_workflow.get("id")
  713. logger.info(f"✅ 工作流创建成功! ID: {workflow_id}, 名称: {workflow_name}")
  714. # 可选:将工作流文件备份到生产服务器
  715. try:
  716. _backup_workflow_to_server(local_path)
  717. except Exception as backup_error:
  718. logger.warning(f"备份工作流文件到服务器失败(非关键): {backup_error}")
  719. return True
  720. except requests.exceptions.Timeout:
  721. logger.error("n8n API 请求超时,请检查网络连接")
  722. return False
  723. except requests.exceptions.ConnectionError:
  724. logger.error(f"无法连接到 n8n 服务器: {api_url}")
  725. return False
  726. except requests.exceptions.HTTPError as e:
  727. error_detail = ""
  728. try:
  729. error_detail = e.response.json()
  730. except Exception:
  731. error_detail = e.response.text
  732. logger.error(
  733. f"n8n API 错误: {e.response.status_code}, 详情: {error_detail}"
  734. )
  735. return False
  736. except Exception as e:
  737. logger.error(f"部署工作流失败: {e}")
  738. import traceback
  739. logger.error(traceback.format_exc())
  740. return False
  741. def _backup_workflow_to_server(local_path: Path) -> bool:
  742. """备份工作流文件到生产服务器(通过 SFTP)"""
  743. try:
  744. import importlib.util
  745. if importlib.util.find_spec("paramiko") is None:
  746. logger.debug("未安装 paramiko 库,跳过文件备份")
  747. return False
  748. remote_path = f"{PRODUCTION_SERVER['workflow_path']}/{local_path.name}"
  749. # 建立 SSH 连接
  750. ssh = get_ssh_connection()
  751. if not ssh:
  752. return False
  753. try:
  754. # 创建 SFTP 客户端
  755. sftp = ssh.open_sftp()
  756. # 确保远程目录存在
  757. try:
  758. sftp.stat(PRODUCTION_SERVER["workflow_path"])
  759. except FileNotFoundError:
  760. logger.info(f"创建远程目录: {PRODUCTION_SERVER['workflow_path']}")
  761. _, stdout, _ = ssh.exec_command(
  762. f"mkdir -p {PRODUCTION_SERVER['workflow_path']}"
  763. )
  764. stdout.channel.recv_exit_status()
  765. # 上传工作流文件
  766. logger.debug(f"备份工作流文件: {local_path} -> {remote_path}")
  767. sftp.put(str(local_path), remote_path)
  768. sftp.close()
  769. ssh.close()
  770. return True
  771. except Exception as e:
  772. logger.warning(f"工作流文件备份失败: {e}")
  773. ssh.close()
  774. return False
  775. except Exception as e:
  776. logger.warning(f"备份工作流失败: {e}")
  777. return False
  778. def find_remote_workflow_files(task_info: dict[str, Any]) -> list[str]:
  779. """
  780. 从生产服务器查找与任务相关的 n8n 工作流文件
  781. 查找策略:
  782. 1. 列出远程 workflow_path 目录下的所有 .json 文件
  783. 2. 根据任务名称或脚本名称匹配相关工作流
  784. Args:
  785. task_info: 任务信息字典
  786. Returns:
  787. 远程工作流文件路径列表
  788. """
  789. remote_files: list[str] = []
  790. code_file = task_info.get("code_file", "")
  791. task_name = task_info.get("task_name", "")
  792. ssh = get_ssh_connection()
  793. if not ssh:
  794. logger.warning("无法连接到生产服务器,跳过远程工作流文件查找")
  795. return remote_files
  796. try:
  797. workflow_path = PRODUCTION_SERVER["workflow_path"]
  798. # 检查目录是否存在
  799. _, stdout, _ = ssh.exec_command(f"test -d {workflow_path} && echo 'exists'")
  800. if stdout.read().decode().strip() != "exists":
  801. logger.info(f"远程工作流目录不存在: {workflow_path}")
  802. ssh.close()
  803. return remote_files
  804. # 列出目录下所有 .json 文件
  805. _, stdout, _ = ssh.exec_command(f"ls -1 {workflow_path}/*.json 2>/dev/null")
  806. file_list = stdout.read().decode().strip().split("\n")
  807. # 过滤有效文件路径
  808. all_json_files = [
  809. f.strip() for f in file_list if f.strip() and f.endswith(".json")
  810. ]
  811. if not all_json_files:
  812. logger.info(f"远程工作流目录 {workflow_path} 中没有 JSON 文件")
  813. ssh.close()
  814. return remote_files
  815. logger.info(f"远程服务器发现 {len(all_json_files)} 个工作流文件")
  816. # 根据任务信息匹配相关工作流
  817. # 构建匹配模式
  818. match_patterns: list[str] = []
  819. # 基于脚本文件名匹配
  820. if code_file and code_file.endswith(".py"):
  821. script_base = code_file[:-3] # 去掉 .py
  822. match_patterns.append(script_base.lower())
  823. # 基于任务名称匹配(针对 DF_DO 格式的任务名)
  824. if task_name:
  825. if task_name.startswith("DF_DO"):
  826. match_patterns.append(task_name.lower())
  827. # 对于中文任务名,尝试提取英文/数字部分
  828. import re
  829. alphanumeric = re.sub(r"[^a-zA-Z0-9_-]", "", task_name)
  830. if alphanumeric and len(alphanumeric) >= 3:
  831. match_patterns.append(alphanumeric.lower())
  832. # 匹配文件
  833. for remote_file in all_json_files:
  834. file_name_lower = Path(remote_file).stem.lower()
  835. # 检查是否与任何模式匹配
  836. matched = False
  837. for pattern in match_patterns:
  838. if pattern in file_name_lower or file_name_lower in pattern:
  839. matched = True
  840. break
  841. if matched and remote_file not in remote_files:
  842. remote_files.append(remote_file)
  843. logger.info(f" 匹配到工作流: {Path(remote_file).name}")
  844. # 如果没有匹配到任何文件,添加所有文件(让用户决定)
  845. # 注意:可以根据业务需求调整此策略
  846. if not remote_files and all_json_files:
  847. logger.info("没有精确匹配的工作流,将尝试部署所有远程工作流文件")
  848. remote_files = all_json_files
  849. ssh.close()
  850. return remote_files
  851. except Exception as e:
  852. logger.error(f"查找远程工作流文件失败: {e}")
  853. if ssh:
  854. ssh.close()
  855. return remote_files
  856. def deploy_remote_workflow_to_n8n(remote_file_path: str) -> bool:
  857. """
  858. 从生产服务器读取工作流 JSON 文件并部署到 n8n 系统
  859. Args:
  860. remote_file_path: 远程服务器上的工作流文件完整路径
  861. Returns:
  862. 是否部署成功
  863. """
  864. try:
  865. import requests
  866. ssh = get_ssh_connection()
  867. if not ssh:
  868. logger.error("无法连接到生产服务器")
  869. return False
  870. # 读取远程工作流文件内容
  871. logger.info(f"从远程服务器读取工作流: {remote_file_path}")
  872. _, stdout, stderr = ssh.exec_command(f"cat {remote_file_path}")
  873. file_content = stdout.read().decode("utf-8")
  874. error_output = stderr.read().decode()
  875. if error_output:
  876. logger.error(f"读取远程文件失败: {error_output}")
  877. ssh.close()
  878. return False
  879. ssh.close()
  880. # 解析工作流 JSON
  881. try:
  882. workflow_data = json.loads(file_content)
  883. except json.JSONDecodeError as e:
  884. logger.error(f"解析工作流 JSON 失败: {e}")
  885. return False
  886. workflow_name = workflow_data.get("name", Path(remote_file_path).stem)
  887. logger.info(f"正在部署工作流到 n8n 服务器: {workflow_name}")
  888. # 获取 n8n API 配置
  889. try:
  890. sys.path.insert(0, str(WORKSPACE_ROOT))
  891. from app.config.config import BaseConfig
  892. api_url = BaseConfig.N8N_API_URL
  893. api_key = BaseConfig.N8N_API_KEY
  894. timeout = BaseConfig.N8N_API_TIMEOUT
  895. except (ImportError, AttributeError):
  896. import os
  897. api_url = os.environ.get("N8N_API_URL", "https://n8n.citupro.com")
  898. api_key = os.environ.get("N8N_API_KEY", "")
  899. timeout = int(os.environ.get("N8N_API_TIMEOUT", "30"))
  900. if not api_key:
  901. logger.error("未配置 N8N_API_KEY,无法部署工作流到 n8n 服务器")
  902. return False
  903. # 准备 API 请求
  904. headers = {
  905. "X-N8N-API-KEY": api_key,
  906. "Content-Type": "application/json",
  907. "Accept": "application/json",
  908. }
  909. # 准备工作流数据
  910. workflow_payload = {
  911. "name": workflow_name,
  912. "nodes": workflow_data.get("nodes", []),
  913. "connections": workflow_data.get("connections", {}),
  914. "settings": workflow_data.get("settings", {}),
  915. }
  916. # 先检查是否已存在同名工作流
  917. list_url = f"{api_url.rstrip('/')}/api/v1/workflows"
  918. try:
  919. list_response = requests.get(
  920. list_url,
  921. headers=headers,
  922. timeout=timeout,
  923. )
  924. if list_response.status_code == 200:
  925. existing_workflows = list_response.json().get("data", [])
  926. existing_wf = None
  927. for wf in existing_workflows:
  928. if wf.get("name") == workflow_name:
  929. existing_wf = wf
  930. break
  931. if existing_wf:
  932. # 更新已存在的工作流
  933. workflow_id = existing_wf.get("id")
  934. update_url = f"{api_url.rstrip('/')}/api/v1/workflows/{workflow_id}"
  935. logger.info(f"发现已存在的工作流 (ID: {workflow_id}),将更新...")
  936. update_response = requests.patch(
  937. update_url,
  938. headers=headers,
  939. json=workflow_payload,
  940. timeout=timeout,
  941. )
  942. update_response.raise_for_status()
  943. logger.info(
  944. f"✅ 工作流更新成功! ID: {workflow_id}, 名称: {workflow_name}"
  945. )
  946. return True
  947. except requests.exceptions.RequestException as e:
  948. logger.debug(f"检查已存在工作流时出错(将尝试创建新工作流): {e}")
  949. # 调用 n8n API 创建工作流
  950. create_url = f"{api_url.rstrip('/')}/api/v1/workflows"
  951. logger.info(f"调用 n8n API 创建工作流: {create_url}")
  952. try:
  953. response = requests.post(
  954. create_url,
  955. headers=headers,
  956. json=workflow_payload,
  957. timeout=timeout,
  958. )
  959. if response.status_code == 401:
  960. logger.error("n8n API 认证失败,请检查 N8N_API_KEY 配置")
  961. return False
  962. elif response.status_code == 403:
  963. logger.error("n8n API 权限不足")
  964. return False
  965. response.raise_for_status()
  966. created_workflow = response.json()
  967. workflow_id = created_workflow.get("id")
  968. logger.info(f"✅ 工作流创建成功! ID: {workflow_id}, 名称: {workflow_name}")
  969. return True
  970. except requests.exceptions.Timeout:
  971. logger.error("n8n API 请求超时,请检查网络连接")
  972. return False
  973. except requests.exceptions.ConnectionError:
  974. logger.error(f"无法连接到 n8n 服务器: {api_url}")
  975. return False
  976. except requests.exceptions.HTTPError as e:
  977. error_detail = ""
  978. try:
  979. error_detail = e.response.json()
  980. except Exception:
  981. error_detail = e.response.text
  982. logger.error(
  983. f"n8n API 错误: {e.response.status_code}, 详情: {error_detail}"
  984. )
  985. return False
  986. except Exception as e:
  987. logger.error(f"从远程服务器部署工作流失败: {e}")
  988. import traceback
  989. logger.error(traceback.format_exc())
  990. return False
  991. def find_related_workflow_files(
  992. task_info: dict[str, Any],
  993. ) -> list[Path]:
  994. """
  995. 查找与任务相关的所有 n8n 工作流文件
  996. 查找策略:
  997. 1. 与脚本同目录的工作流文件 (n8n_workflow_*.json)
  998. 2. datafactory/n8n_workflows 目录下的工作流文件
  999. 3. 根据任务名称模式匹配
  1000. 4. 根据脚本名称匹配 (去掉 .py 后缀)
  1001. 5. 根据任务 ID 匹配
  1002. 6. 最近修改的工作流文件 (在任务创建后修改的)
  1003. """
  1004. workflow_files: list[Path] = []
  1005. code_name = task_info.get("code_name", "")
  1006. code_path = task_info.get("code_path", "datafactory/scripts")
  1007. task_name = task_info.get("task_name", "")
  1008. task_id = task_info.get("task_id")
  1009. # 获取任务通知时间用于判断文件是否是新创建的
  1010. notified_at_str = task_info.get("notified_at", "")
  1011. notified_at = None
  1012. if notified_at_str:
  1013. with contextlib.suppress(ValueError, TypeError):
  1014. notified_at = datetime.fromisoformat(notified_at_str.replace("Z", "+00:00"))
  1015. # 查找模式1: 与脚本同目录的工作流文件
  1016. script_dir = WORKSPACE_ROOT / code_path
  1017. if script_dir.exists() and script_dir.is_dir():
  1018. for wf_file in script_dir.glob("n8n_workflow_*.json"):
  1019. if wf_file.is_file() and wf_file not in workflow_files:
  1020. workflow_files.append(wf_file)
  1021. # 也查找以 workflow_ 开头的文件
  1022. for wf_file in script_dir.glob("workflow_*.json"):
  1023. if wf_file.is_file() and wf_file not in workflow_files:
  1024. workflow_files.append(wf_file)
  1025. # 查找模式2: datafactory/n8n_workflows 目录
  1026. n8n_workflows_dir = WORKSPACE_ROOT / "datafactory" / "n8n_workflows"
  1027. if n8n_workflows_dir.exists():
  1028. for wf_file in n8n_workflows_dir.glob("*.json"):
  1029. if wf_file.is_file() and wf_file not in workflow_files:
  1030. workflow_files.append(wf_file)
  1031. # 查找模式3: 根据任务名称匹配
  1032. if task_name and task_name != "未知任务":
  1033. # 尝试多种名称变体
  1034. name_patterns = [
  1035. task_name.replace(" ", "_").lower(),
  1036. task_name.replace(" ", "-").lower(),
  1037. task_name.lower(),
  1038. ]
  1039. for pattern in name_patterns:
  1040. if len(pattern) < 3: # 跳过过短的模式
  1041. continue
  1042. for wf_file in (WORKSPACE_ROOT / "datafactory").rglob(f"*{pattern}*.json"):
  1043. # 验证是文件、未添加过、且是有效的 n8n 工作流文件
  1044. if (
  1045. wf_file.is_file()
  1046. and wf_file not in workflow_files
  1047. and _is_n8n_workflow_file(wf_file)
  1048. ):
  1049. workflow_files.append(wf_file)
  1050. # 查找模式4: 根据脚本名称匹配
  1051. if code_name and code_name.endswith(".py"):
  1052. script_base_name = code_name[:-3] # 去掉 .py
  1053. # 在 datafactory 目录下查找
  1054. for wf_file in (WORKSPACE_ROOT / "datafactory").rglob(
  1055. f"*{script_base_name}*.json"
  1056. ):
  1057. if (
  1058. wf_file.is_file()
  1059. and wf_file not in workflow_files
  1060. and _is_n8n_workflow_file(wf_file)
  1061. ):
  1062. workflow_files.append(wf_file)
  1063. # 查找模式5: 根据任务 ID 匹配
  1064. if task_id:
  1065. for wf_file in (WORKSPACE_ROOT / "datafactory").rglob(f"*task_{task_id}*.json"):
  1066. if (
  1067. wf_file.is_file()
  1068. and wf_file not in workflow_files
  1069. and _is_n8n_workflow_file(wf_file)
  1070. ):
  1071. workflow_files.append(wf_file)
  1072. # 查找模式6: 最近修改的工作流文件(在任务创建后修改的)
  1073. if notified_at:
  1074. for wf_file in (WORKSPACE_ROOT / "datafactory").rglob("*.json"):
  1075. if wf_file.is_file() and wf_file not in workflow_files:
  1076. try:
  1077. mtime = datetime.fromtimestamp(wf_file.stat().st_mtime)
  1078. # 如果文件在任务通知后被修改,可能是相关的工作流
  1079. if mtime > notified_at.replace(
  1080. tzinfo=None
  1081. ) and _is_n8n_workflow_file(wf_file):
  1082. workflow_files.append(wf_file)
  1083. logger.debug(f"发现最近修改的工作流: {wf_file.name}")
  1084. except (OSError, ValueError):
  1085. pass
  1086. return workflow_files
  1087. def _is_n8n_workflow_file(file_path: Path) -> bool:
  1088. """
  1089. 检查文件是否是有效的 n8n 工作流文件
  1090. 通过检查 JSON 结构来验证
  1091. """
  1092. try:
  1093. with open(file_path, encoding="utf-8") as f:
  1094. data = json.load(f)
  1095. # n8n 工作流文件通常包含 nodes 和 connections 字段
  1096. if isinstance(data, dict):
  1097. has_nodes = "nodes" in data
  1098. has_connections = "connections" in data
  1099. has_name = "name" in data
  1100. # 至少需要有 nodes 或符合 n8n 工作流特征
  1101. return has_nodes or (has_name and has_connections)
  1102. return False
  1103. except (json.JSONDecodeError, OSError):
  1104. return False
  1105. def auto_deploy_completed_task(task_info: dict[str, Any]) -> bool:
  1106. """
  1107. 自动部署已完成任务的脚本和工作流到生产服务器
  1108. 部署流程:
  1109. 1. 部署 Python 脚本到生产服务器 (通过 SFTP)
  1110. 2. 查找并部署相关的 n8n 工作流 (通过 n8n API)
  1111. 3. 记录部署结果
  1112. """
  1113. # 优先使用 code_file 字段,其次使用 code_name
  1114. code_file = task_info.get("code_file", "")
  1115. code_name = task_info.get("code_name", "")
  1116. code_path = task_info.get("code_path", "datafactory/scripts")
  1117. task_name = task_info.get("task_name", "未知任务")
  1118. task_id = task_info.get("task_id", "N/A")
  1119. # 确定实际的脚本文件名:优先使用 code_file,如果为空则尝试 code_name
  1120. actual_script_file = code_file if code_file else code_name
  1121. if not actual_script_file or not code_path:
  1122. logger.warning(f"任务 {task_name} (ID: {task_id}) 缺少代码文件信息,跳过部署")
  1123. return False
  1124. logger.info("=" * 60)
  1125. logger.info(f"🚀 开始自动部署任务: {task_name} (ID: {task_id})")
  1126. logger.info("=" * 60)
  1127. deploy_results = {
  1128. "script_deployed": False,
  1129. "workflows_found": 0,
  1130. "workflows_deployed": 0,
  1131. "workflows_failed": 0,
  1132. }
  1133. # 1. 部署 Python 脚本
  1134. if actual_script_file.endswith(".py"):
  1135. script_path = f"{code_path}/{actual_script_file}"
  1136. logger.info(f"📦 部署 Python 脚本: {script_path}")
  1137. if deploy_script_to_production(script_path):
  1138. logger.info(f"✅ 脚本 {actual_script_file} 部署成功")
  1139. deploy_results["script_deployed"] = True
  1140. else:
  1141. logger.error(f"❌ 脚本 {actual_script_file} 部署失败")
  1142. # 2. 查找并部署相关的 n8n 工作流文件
  1143. # 2.1 首先从本地查找工作流文件
  1144. logger.info("🔍 查找本地 n8n 工作流文件...")
  1145. workflow_files = find_related_workflow_files(task_info)
  1146. if workflow_files:
  1147. logger.info(f"📋 本地发现 {len(workflow_files)} 个相关工作流文件:")
  1148. for wf_file in workflow_files:
  1149. logger.info(f" - {wf_file.relative_to(WORKSPACE_ROOT)}")
  1150. for wf_file in workflow_files:
  1151. logger.info(f"🔄 部署本地工作流: {wf_file.name}")
  1152. if deploy_n8n_workflow_to_production(str(wf_file)):
  1153. logger.info(f"✅ 工作流 {wf_file.name} 部署成功")
  1154. deploy_results["workflows_deployed"] += 1
  1155. else:
  1156. logger.error(f"❌ 工作流 {wf_file.name} 部署失败")
  1157. deploy_results["workflows_failed"] += 1
  1158. else:
  1159. logger.info("ℹ️ 本地未发现相关工作流文件")
  1160. # 2.2 然后从生产服务器查找并部署工作流文件
  1161. logger.info("🔍 查找生产服务器上的 n8n 工作流文件...")
  1162. remote_workflow_files = find_remote_workflow_files(task_info)
  1163. if remote_workflow_files:
  1164. logger.info(f"📋 远程服务器发现 {len(remote_workflow_files)} 个相关工作流文件:")
  1165. for remote_file in remote_workflow_files:
  1166. logger.info(f" - {Path(remote_file).name}")
  1167. for remote_file in remote_workflow_files:
  1168. logger.info(f"🔄 部署远程工作流: {Path(remote_file).name}")
  1169. if deploy_remote_workflow_to_n8n(remote_file):
  1170. logger.info(f"✅ 远程工作流 {Path(remote_file).name} 部署成功")
  1171. deploy_results["workflows_deployed"] += 1
  1172. else:
  1173. logger.error(f"❌ 远程工作流 {Path(remote_file).name} 部署失败")
  1174. deploy_results["workflows_failed"] += 1
  1175. else:
  1176. logger.info("ℹ️ 远程服务器未发现相关工作流文件")
  1177. # 更新发现的工作流总数
  1178. deploy_results["workflows_found"] = len(workflow_files) + len(remote_workflow_files)
  1179. # 3. 汇总部署结果
  1180. logger.info("=" * 60)
  1181. logger.info(f"📊 部署结果汇总 - 任务: {task_name} (ID: {task_id})")
  1182. logger.info("-" * 40)
  1183. logger.info(
  1184. f" 脚本部署: {'✅ 成功' if deploy_results['script_deployed'] else '❌ 失败或跳过'}"
  1185. )
  1186. logger.info(f" 发现工作流: {deploy_results['workflows_found']} 个")
  1187. logger.info(f" 工作流部署成功: {deploy_results['workflows_deployed']} 个")
  1188. logger.info(f" 工作流部署失败: {deploy_results['workflows_failed']} 个")
  1189. # 判断整体部署是否成功
  1190. deploy_success = (
  1191. deploy_results["script_deployed"] and deploy_results["workflows_failed"] == 0
  1192. )
  1193. if deploy_success:
  1194. logger.info(f"✅ 任务 {task_name} 部署完成!")
  1195. elif deploy_results["script_deployed"]:
  1196. if deploy_results["workflows_failed"] > 0:
  1197. logger.warning(f"⚠️ 任务 {task_name} 脚本部署成功,但部分工作流部署失败")
  1198. else:
  1199. logger.info(f"✅ 任务 {task_name} 脚本部署成功")
  1200. deploy_success = True # 脚本部署成功就认为整体成功
  1201. else:
  1202. logger.error(f"❌ 任务 {task_name} 部署失败")
  1203. logger.info("=" * 60)
  1204. return deploy_success
  1205. # ============================================================================
  1206. # Cursor Agent 自动化
  1207. # ============================================================================
  1208. # Agent 会话状态
  1209. AGENT_SESSION_ACTIVE: bool = False
  1210. AGENT_START_TIME: float = 0
  1211. def get_all_cursor_windows() -> list[dict[str, Any]]:
  1212. """获取所有 Cursor 窗口信息"""
  1213. if not HAS_CURSOR_GUI:
  1214. return []
  1215. cursor_windows: list[dict[str, Any]] = []
  1216. def enum_windows_callback(hwnd, _extra):
  1217. if win32gui.IsWindowVisible(hwnd):
  1218. title = win32gui.GetWindowText(hwnd) or ""
  1219. class_name = win32gui.GetClassName(hwnd) or ""
  1220. is_cursor = "cursor" in title.lower()
  1221. if class_name and "chrome_widgetwin" in class_name.lower():
  1222. is_cursor = True
  1223. if is_cursor:
  1224. left, top, right, bottom = win32gui.GetWindowRect(hwnd)
  1225. area = (right - left) * (bottom - top)
  1226. cursor_windows.append(
  1227. {
  1228. "hwnd": hwnd,
  1229. "title": title,
  1230. "class_name": class_name,
  1231. "area": area,
  1232. }
  1233. )
  1234. return True
  1235. win32gui.EnumWindows(enum_windows_callback, None)
  1236. return cursor_windows
  1237. def find_cursor_window() -> int | None:
  1238. """查找 Cursor 主窗口句柄"""
  1239. if not HAS_CURSOR_GUI:
  1240. return None
  1241. cursor_windows = get_all_cursor_windows()
  1242. if not cursor_windows:
  1243. logger.warning("未找到 Cursor 窗口")
  1244. return None
  1245. # 按面积排序,返回最大的窗口(主窗口)
  1246. cursor_windows.sort(key=lambda x: x["area"], reverse=True)
  1247. return cursor_windows[0]["hwnd"]
  1248. def activate_window(hwnd: int) -> bool:
  1249. """
  1250. 激活指定窗口
  1251. Windows 对 SetForegroundWindow 有限制,只有满足以下条件之一才能成功:
  1252. 1. 调用进程是前台进程
  1253. 2. 调用进程由前台进程启动
  1254. 3. 目标窗口属于前台进程
  1255. 4. 没有其他窗口在前台
  1256. 此函数使用多种技巧绕过这些限制。
  1257. """
  1258. if not HAS_CURSOR_GUI:
  1259. return False
  1260. try:
  1261. # 方法1: 使用 AttachThreadInput 技巧绕过 SetForegroundWindow 限制
  1262. # 这是最可靠的方法,通过将当前线程附加到前台窗口的线程来获取激活权限
  1263. import ctypes
  1264. user32 = ctypes.windll.user32
  1265. # 获取当前前台窗口的线程ID
  1266. foreground_hwnd = user32.GetForegroundWindow()
  1267. foreground_thread_id = user32.GetWindowThreadProcessId(foreground_hwnd, None)
  1268. # 获取当前线程ID
  1269. current_thread_id = ctypes.windll.kernel32.GetCurrentThreadId()
  1270. attached = False
  1271. # 如果当前线程不是前台线程,则附加到前台线程
  1272. if current_thread_id != foreground_thread_id:
  1273. attached = user32.AttachThreadInput(
  1274. current_thread_id, foreground_thread_id, True
  1275. )
  1276. try:
  1277. # 先确保窗口不是最小化状态
  1278. if win32gui.IsIconic(hwnd):
  1279. win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
  1280. time.sleep(0.2)
  1281. # 使用 BringWindowToTop 将窗口置顶
  1282. user32.BringWindowToTop(hwnd)
  1283. # 显示窗口
  1284. win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
  1285. # 尝试 SetForegroundWindow
  1286. result = user32.SetForegroundWindow(hwnd)
  1287. if not result:
  1288. # 方法2: 使用 Alt 键模拟技巧
  1289. # 发送一个 Alt 键可以让系统认为用户有交互意图
  1290. # 定义必要的常量
  1291. KEYEVENTF_EXTENDEDKEY = 0x0001
  1292. KEYEVENTF_KEYUP = 0x0002
  1293. VK_MENU = 0x12 # Alt 键
  1294. # 模拟按下和释放 Alt 键
  1295. user32.keybd_event(VK_MENU, 0, KEYEVENTF_EXTENDEDKEY, 0)
  1296. user32.keybd_event(
  1297. VK_MENU, 0, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0
  1298. )
  1299. time.sleep(0.1)
  1300. # 再次尝试
  1301. result = user32.SetForegroundWindow(hwnd)
  1302. if not result:
  1303. # 方法3: 使用 ShowWindow 配合 SW_SHOWDEFAULT
  1304. win32gui.ShowWindow(hwnd, win32con.SW_SHOWDEFAULT)
  1305. time.sleep(0.1)
  1306. result = user32.SetForegroundWindow(hwnd)
  1307. if not result:
  1308. # 方法4: 使用 SetWindowPos 将窗口置于最顶层
  1309. SWP_NOMOVE = 0x0002
  1310. SWP_NOSIZE = 0x0001
  1311. SWP_SHOWWINDOW = 0x0040
  1312. HWND_TOPMOST = -1
  1313. HWND_NOTOPMOST = -2
  1314. # 先设为最顶层
  1315. user32.SetWindowPos(
  1316. hwnd,
  1317. HWND_TOPMOST,
  1318. 0,
  1319. 0,
  1320. 0,
  1321. 0,
  1322. SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
  1323. )
  1324. time.sleep(0.1)
  1325. # 再取消最顶层(但窗口仍在前台)
  1326. user32.SetWindowPos(
  1327. hwnd,
  1328. HWND_NOTOPMOST,
  1329. 0,
  1330. 0,
  1331. 0,
  1332. 0,
  1333. SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
  1334. )
  1335. result = user32.SetForegroundWindow(hwnd)
  1336. time.sleep(0.3)
  1337. # 验证是否成功
  1338. current_foreground = user32.GetForegroundWindow()
  1339. if current_foreground == hwnd:
  1340. logger.debug("窗口激活成功")
  1341. return True
  1342. else:
  1343. # 即使 SetForegroundWindow 返回失败,窗口可能已经被置顶并可见
  1344. # 检查窗口是否可见且不是最小化
  1345. if win32gui.IsWindowVisible(hwnd) and not win32gui.IsIconic(hwnd):
  1346. logger.warning("窗口可能未完全激活到前台,但窗口可见,继续执行...")
  1347. return True
  1348. else:
  1349. logger.error("激活窗口失败: 窗口不在前台")
  1350. return False
  1351. finally:
  1352. # 分离线程
  1353. if attached:
  1354. user32.AttachThreadInput(current_thread_id, foreground_thread_id, False)
  1355. except Exception as e:
  1356. logger.error(f"激活窗口失败: {e}")
  1357. # 最后的备用方案:直接尝试基本操作
  1358. try:
  1359. win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
  1360. win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
  1361. time.sleep(0.3)
  1362. # 即使失败也返回 True,让调用者继续尝试
  1363. if win32gui.IsWindowVisible(hwnd):
  1364. logger.warning("使用备用方案激活窗口,继续执行...")
  1365. return True
  1366. except Exception:
  1367. pass
  1368. return False
  1369. def open_new_agent() -> bool:
  1370. """在 Cursor 中打开新的 Agent 窗口"""
  1371. global AGENT_SESSION_ACTIVE, AGENT_START_TIME
  1372. if not HAS_CURSOR_GUI:
  1373. logger.warning("当前环境不支持 Cursor GUI 自动化")
  1374. return False
  1375. hwnd = find_cursor_window()
  1376. if not hwnd:
  1377. return False
  1378. if not activate_window(hwnd):
  1379. return False
  1380. try:
  1381. # 使用 Ctrl+Shift+I 打开新的 Agent/Composer
  1382. logger.info("正在打开新的 Agent...")
  1383. pyautogui.hotkey("ctrl", "shift", "i")
  1384. time.sleep(2.0) # 等待 Agent 窗口打开
  1385. AGENT_SESSION_ACTIVE = True
  1386. AGENT_START_TIME = time.time()
  1387. logger.info("✅ 新的 Agent 已打开")
  1388. return True
  1389. except Exception as e:
  1390. logger.error(f"打开 Agent 失败: {e}")
  1391. return False
  1392. def close_current_agent(force: bool = False, max_retries: int = 3) -> bool:
  1393. """
  1394. 关闭当前的 Agent 会话
  1395. Args:
  1396. force: 是否强制关闭(使用多种方法)
  1397. max_retries: 最大重试次数
  1398. 关闭策略:
  1399. 1. 使用 Escape 键关闭 Agent 面板
  1400. 2. 如果失败,尝试 Ctrl+Shift+I 切换 Agent 面板
  1401. 3. 如果仍失败,尝试点击空白区域并按 Escape
  1402. """
  1403. global AGENT_SESSION_ACTIVE
  1404. if not HAS_CURSOR_GUI:
  1405. AGENT_SESSION_ACTIVE = False
  1406. return False
  1407. if not AGENT_SESSION_ACTIVE and not force:
  1408. logger.info("没有活动的 Agent 会话")
  1409. return True
  1410. logger.info("🔄 正在关闭 Agent...")
  1411. for attempt in range(max_retries):
  1412. try:
  1413. hwnd = find_cursor_window()
  1414. if not hwnd:
  1415. logger.warning("未找到 Cursor 窗口")
  1416. AGENT_SESSION_ACTIVE = False
  1417. return False
  1418. if not activate_window(hwnd):
  1419. logger.warning(f"激活窗口失败 (尝试 {attempt + 1}/{max_retries})")
  1420. time.sleep(0.5)
  1421. continue
  1422. # 方法1: 按 Escape 键关闭 Agent
  1423. logger.debug(f"尝试方法1: Escape 键 (尝试 {attempt + 1}/{max_retries})")
  1424. pyautogui.press("escape")
  1425. time.sleep(0.3)
  1426. pyautogui.press("escape")
  1427. time.sleep(0.3)
  1428. # 方法2: 使用 Ctrl+Shift+I 切换 Agent 面板(关闭)
  1429. if force or attempt > 0:
  1430. logger.debug("尝试方法2: Ctrl+Shift+I 切换")
  1431. pyautogui.hotkey("ctrl", "shift", "i")
  1432. time.sleep(0.5)
  1433. # 方法3: 点击编辑器区域并按 Escape
  1434. if force or attempt > 1:
  1435. logger.debug("尝试方法3: 点击编辑器区域")
  1436. # 获取窗口位置,点击中心偏左位置(编辑器区域)
  1437. try:
  1438. left, top, right, bottom = win32gui.GetWindowRect(hwnd)
  1439. center_x = left + (right - left) // 3 # 偏左1/3位置
  1440. center_y = top + (bottom - top) // 2
  1441. pyautogui.click(center_x, center_y)
  1442. time.sleep(0.2)
  1443. pyautogui.press("escape")
  1444. time.sleep(0.3)
  1445. except Exception as click_err:
  1446. logger.debug(f"点击方法失败: {click_err}")
  1447. AGENT_SESSION_ACTIVE = False
  1448. logger.info("✅ Agent 已关闭")
  1449. return True
  1450. except Exception as e:
  1451. logger.warning(f"关闭 Agent 尝试 {attempt + 1} 失败: {e}")
  1452. time.sleep(0.5)
  1453. # 即使关闭失败,也标记为非活动状态,避免状态不一致
  1454. AGENT_SESSION_ACTIVE = False
  1455. logger.warning("⚠️ Agent 关闭可能未完全成功,但已重置状态")
  1456. return False
  1457. def force_close_all_agents() -> bool:
  1458. """
  1459. 强制关闭所有可能的 Agent 会话
  1460. 用于清理可能遗留的多个 Agent 窗口
  1461. """
  1462. global AGENT_SESSION_ACTIVE
  1463. if not HAS_CURSOR_GUI:
  1464. return False
  1465. logger.info("🔄 强制关闭所有 Agent 会话...")
  1466. try:
  1467. hwnd = find_cursor_window()
  1468. if not hwnd:
  1469. AGENT_SESSION_ACTIVE = False
  1470. return True
  1471. if not activate_window(hwnd):
  1472. AGENT_SESSION_ACTIVE = False
  1473. return False
  1474. # 连续按多次 Escape 确保关闭所有面板
  1475. for _ in range(5):
  1476. pyautogui.press("escape")
  1477. time.sleep(0.2)
  1478. # 使用快捷键关闭可能的 Agent 面板
  1479. pyautogui.hotkey("ctrl", "shift", "i")
  1480. time.sleep(0.3)
  1481. pyautogui.hotkey("ctrl", "shift", "i")
  1482. time.sleep(0.3)
  1483. AGENT_SESSION_ACTIVE = False
  1484. logger.info("✅ 所有 Agent 会话已关闭")
  1485. return True
  1486. except Exception as e:
  1487. logger.error(f"强制关闭 Agent 失败: {e}")
  1488. AGENT_SESSION_ACTIVE = False
  1489. return False
  1490. def type_message_to_agent(message: str) -> bool:
  1491. """向 Agent 输入消息"""
  1492. if not HAS_CURSOR_GUI:
  1493. return False
  1494. try:
  1495. # 等待 Agent 输入框获得焦点
  1496. time.sleep(0.5)
  1497. # 使用剪贴板粘贴(更可靠地处理中文和特殊字符)
  1498. if HAS_PYPERCLIP:
  1499. try:
  1500. pyperclip.copy(message)
  1501. pyautogui.hotkey("ctrl", "v")
  1502. time.sleep(0.5)
  1503. except Exception:
  1504. # 回退到逐字符输入
  1505. pyautogui.write(message, interval=0.03)
  1506. else:
  1507. pyautogui.write(message, interval=0.03)
  1508. time.sleep(0.3)
  1509. # 按 Enter 发送消息
  1510. pyautogui.press("enter")
  1511. logger.info("✅ 消息已发送到 Agent")
  1512. return True
  1513. except Exception as e:
  1514. logger.error(f"发送消息到 Agent 失败: {e}")
  1515. return False
  1516. def wait_for_agent_completion(
  1517. timeout: int = 3600,
  1518. check_interval: int = 30,
  1519. ) -> bool:
  1520. """
  1521. 等待 Agent 完成任务
  1522. 通过检查 pending_tasks.json 中的任务状态来判断是否完成
  1523. """
  1524. start_time = time.time()
  1525. logger.info(f"等待 Agent 完成任务(超时: {timeout}s)...")
  1526. while time.time() - start_time < timeout:
  1527. processing_ids = get_processing_task_ids()
  1528. if not processing_ids:
  1529. elapsed = int(time.time() - start_time)
  1530. logger.info(f"✅ 所有任务已完成!耗时: {elapsed}s")
  1531. return True
  1532. remaining = len(processing_ids)
  1533. elapsed = int(time.time() - start_time)
  1534. logger.info(
  1535. f"仍有 {remaining} 个任务进行中... (已等待 {elapsed}s / {timeout}s)"
  1536. )
  1537. time.sleep(check_interval)
  1538. logger.warning("等待超时,仍有未完成的任务")
  1539. return False
  1540. def run_agent_once(
  1541. timeout: int = 3600,
  1542. auto_close: bool = True,
  1543. ) -> bool:
  1544. """
  1545. 执行一次 Agent 任务
  1546. 流程:
  1547. 1. 同步已完成任务到数据库
  1548. 2. 从数据库读取 pending 任务
  1549. 3. 更新任务状态为 processing
  1550. 4. 生成执行指令文件(包含所有 processing 任务)
  1551. 5. 打开 Agent 并发送消息
  1552. 6. 等待任务完成
  1553. 7. 同步完成任务 + 自动部署
  1554. 8. 关闭 Agent
  1555. """
  1556. logger.info("=" * 60)
  1557. logger.info("Agent 单次执行模式")
  1558. logger.info("=" * 60)
  1559. # 1. 先同步已完成任务
  1560. sync_completed_tasks_to_db()
  1561. # 2. 从数据库获取 pending 任务
  1562. logger.info("正在从数据库查询 pending 任务...")
  1563. pending_tasks = get_pending_tasks()
  1564. # 3. 获取当前 processing 任务
  1565. processing_ids = get_processing_task_ids()
  1566. # 4. 检查是否有任务需要执行
  1567. if not pending_tasks and not processing_ids:
  1568. logger.info("✅ 没有待执行的任务")
  1569. return True
  1570. if pending_tasks:
  1571. logger.info(f"发现 {len(pending_tasks)} 个新的 pending 任务")
  1572. # 5. 更新新任务状态为 processing
  1573. for task in pending_tasks:
  1574. update_task_status(task["task_id"], "processing")
  1575. # 6. 写入 pending_tasks.json
  1576. write_pending_tasks_json(pending_tasks)
  1577. if processing_ids:
  1578. logger.info(f"发现 {len(processing_ids)} 个已有的 processing 任务")
  1579. # 7. 获取所有需要执行的任务(包含完整信息)并生成执行指令
  1580. all_tasks_to_execute = get_all_tasks_to_execute()
  1581. if all_tasks_to_execute:
  1582. logger.info(f"共 {len(all_tasks_to_execute)} 个任务需要执行")
  1583. # 生成包含所有任务的执行指令文件
  1584. create_execute_instructions(all_tasks_to_execute)
  1585. else:
  1586. logger.warning("无法获取任务详细信息,跳过生成执行指令")
  1587. # 7. 更新触发器文件
  1588. all_processing_ids = get_processing_task_ids()
  1589. if all_processing_ids:
  1590. update_trigger_file(
  1591. task_count=len(all_processing_ids),
  1592. status="有待执行任务",
  1593. task_ids=all_processing_ids,
  1594. )
  1595. # 8. 打开 Agent 并发送消息
  1596. if not open_new_agent():
  1597. logger.error("❌ 无法打开 Agent")
  1598. return False
  1599. if not type_message_to_agent(AGENT_MESSAGE):
  1600. logger.error("❌ 无法发送消息到 Agent")
  1601. close_current_agent()
  1602. return False
  1603. logger.info(f"已发送消息: {AGENT_MESSAGE[:50]}...")
  1604. # 9. 等待任务完成
  1605. completed = wait_for_agent_completion(timeout=timeout)
  1606. # 10. 立即关闭 Agent(在同步之前)
  1607. logger.info("🔄 任务执行完毕,立即关闭 Agent...")
  1608. if auto_close:
  1609. close_current_agent(force=True)
  1610. time.sleep(1.0) # 等待关闭完成
  1611. # 11. 同步已完成的任务到数据库(触发自动部署)
  1612. logger.info("🔄 开始同步和部署...")
  1613. sync_completed_tasks_to_db()
  1614. if completed:
  1615. logger.info("✅ Agent 已完成所有任务")
  1616. else:
  1617. logger.warning("⚠️ Agent 未能在超时时间内完成所有任务")
  1618. # 强制关闭可能遗留的 Agent
  1619. force_close_all_agents()
  1620. logger.info("=" * 60)
  1621. logger.info("Agent 会话结束")
  1622. logger.info("=" * 60)
  1623. return completed
  1624. def run_agent_loop(
  1625. interval: int = 300,
  1626. timeout: int = 3600,
  1627. auto_close: bool = True,
  1628. ) -> None:
  1629. """
  1630. Agent 循环模式
  1631. 循环执行 Agent 单次任务,直到用户按 Ctrl+C 停止
  1632. 完整流程:
  1633. 1. 同步已完成任务到数据库(触发自动部署)
  1634. 2. 检查是否有新的 pending 任务
  1635. 3. 生成执行指令文件
  1636. 4. 启动 Agent 执行任务
  1637. 5. 等待任务完成
  1638. 6. 同步完成任务并触发自动部署
  1639. 7. 循环...
  1640. """
  1641. global AGENT_SESSION_ACTIVE
  1642. logger.info("=" * 60)
  1643. logger.info("🔄 Agent 循环模式已启动")
  1644. logger.info("=" * 60)
  1645. logger.info(f" 检查间隔: {interval} 秒")
  1646. logger.info(f" 任务超时: {timeout} 秒")
  1647. logger.info(f" 自动部署: {'✅ 已启用' if ENABLE_AUTO_DEPLOY else '❌ 已禁用'}")
  1648. logger.info(f" 自动关闭 Agent: {'✅ 是' if auto_close else '❌ 否'}")
  1649. logger.info("=" * 60)
  1650. logger.info("按 Ctrl+C 停止服务")
  1651. logger.info("=" * 60)
  1652. loop_count = 0
  1653. total_tasks_completed = 0
  1654. total_deployments = 0
  1655. try:
  1656. while True:
  1657. try:
  1658. loop_count += 1
  1659. logger.info(f"\n{'=' * 60}")
  1660. logger.info(f"📍 开始第 {loop_count} 轮任务检查...")
  1661. logger.info(f"{'=' * 60}")
  1662. # 1. 同步已完成任务(这会触发自动部署)
  1663. logger.info("🔄 检查并同步已完成的任务...")
  1664. synced_count = sync_completed_tasks_to_db()
  1665. if synced_count > 0:
  1666. total_tasks_completed += synced_count
  1667. total_deployments += synced_count
  1668. logger.info(
  1669. f"✅ 已同步 {synced_count} 个完成的任务(累计: {total_tasks_completed})"
  1670. )
  1671. # 2. 从数据库获取 pending 任务
  1672. logger.info("📡 检查数据库中的 pending 任务...")
  1673. pending_tasks = get_pending_tasks()
  1674. if pending_tasks:
  1675. logger.info(f"📋 发现 {len(pending_tasks)} 个新的 pending 任务:")
  1676. for task in pending_tasks:
  1677. logger.info(f" - [{task['task_id']}] {task['task_name']}")
  1678. # 更新任务状态为 processing
  1679. for task in pending_tasks:
  1680. update_task_status(task["task_id"], "processing")
  1681. # 写入 pending_tasks.json
  1682. write_pending_tasks_json(pending_tasks)
  1683. # 3. 检查是否有 processing 任务
  1684. processing_ids = get_processing_task_ids()
  1685. # 4. 如果有新任务或有 processing 任务,生成包含所有任务的执行指令
  1686. if pending_tasks or processing_ids:
  1687. all_tasks_to_execute = get_all_tasks_to_execute()
  1688. if all_tasks_to_execute:
  1689. logger.info(
  1690. f"📝 生成执行指令文件,共 {len(all_tasks_to_execute)} 个任务"
  1691. )
  1692. create_execute_instructions(all_tasks_to_execute)
  1693. if processing_ids:
  1694. # 如果有活动的 Agent 会话,不需要重新启动
  1695. if AGENT_SESSION_ACTIVE:
  1696. logger.info(
  1697. f"⏳ Agent 正在执行中,剩余 {len(processing_ids)} 个任务"
  1698. )
  1699. else:
  1700. logger.info(
  1701. f"🎯 发现 {len(processing_ids)} 个待处理任务,准备启动 Agent"
  1702. )
  1703. # 更新触发器文件
  1704. update_trigger_file(
  1705. task_count=len(processing_ids),
  1706. status="有待执行任务",
  1707. task_ids=processing_ids,
  1708. )
  1709. # 启动 Agent
  1710. if open_new_agent():
  1711. if type_message_to_agent(AGENT_MESSAGE):
  1712. logger.info("✅ 已启动 Agent 并发送执行提醒")
  1713. # 等待任务完成
  1714. task_completed = wait_for_agent_completion(
  1715. timeout=timeout
  1716. )
  1717. # ===== 关键:任务完成后立即关闭 Agent =====
  1718. logger.info("🔄 任务执行完毕,立即关闭 Agent...")
  1719. if auto_close:
  1720. # 使用强制关闭,确保 Agent 被正确关闭
  1721. close_current_agent(force=True)
  1722. # 等待一小段时间确保关闭完成
  1723. time.sleep(1.0)
  1724. # 同步完成的任务(这会触发自动部署)
  1725. logger.info("🔄 开始同步和部署...")
  1726. synced = sync_completed_tasks_to_db()
  1727. if synced > 0:
  1728. total_tasks_completed += synced
  1729. total_deployments += synced
  1730. logger.info(
  1731. f"✅ 本轮完成 {synced} 个任务的同步和部署"
  1732. )
  1733. # 显示本轮统计
  1734. logger.info(f"📊 本轮统计: 完成任务 {synced} 个")
  1735. if ENABLE_AUTO_DEPLOY:
  1736. logger.info(f" 已触发自动部署: {synced} 个")
  1737. # 如果任务未完成(超时),也确保关闭 Agent
  1738. if not task_completed:
  1739. logger.warning("⚠️ 任务超时,强制关闭 Agent")
  1740. force_close_all_agents()
  1741. else:
  1742. logger.warning("❌ 发送消息失败")
  1743. close_current_agent(force=True)
  1744. else:
  1745. logger.warning("❌ 启动 Agent 失败")
  1746. else:
  1747. logger.info("✅ 当前没有待处理任务")
  1748. # 显示累计统计
  1749. logger.info(
  1750. f"\n📈 累计统计: 已完成 {total_tasks_completed} 个任务, "
  1751. f"已部署 {total_deployments} 个"
  1752. )
  1753. logger.info(f"⏰ {interval} 秒后将进行第 {loop_count + 1} 轮检查...")
  1754. time.sleep(interval)
  1755. except KeyboardInterrupt:
  1756. raise
  1757. except Exception as e:
  1758. logger.error(f"❌ 执行出错: {e}")
  1759. import traceback
  1760. logger.error(traceback.format_exc())
  1761. logger.info(f"⏰ {interval} 秒后重试...")
  1762. time.sleep(interval)
  1763. except KeyboardInterrupt:
  1764. # 退出时关闭 Agent
  1765. logger.info("\n" + "=" * 60)
  1766. logger.info("⛔ 收到停止信号,正在退出...")
  1767. if AGENT_SESSION_ACTIVE:
  1768. logger.info("🔄 正在关闭 Agent...")
  1769. close_current_agent()
  1770. # 最后一次同步
  1771. logger.info("🔄 执行最终同步...")
  1772. final_synced = sync_completed_tasks_to_db()
  1773. if final_synced > 0:
  1774. total_tasks_completed += final_synced
  1775. logger.info(f"✅ 最终同步了 {final_synced} 个任务")
  1776. logger.info("=" * 60)
  1777. logger.info("📊 会话统计:")
  1778. logger.info(f" 总循环次数: {loop_count}")
  1779. logger.info(f" 总完成任务: {total_tasks_completed}")
  1780. logger.info(f" 总部署次数: {total_deployments}")
  1781. logger.info("=" * 60)
  1782. logger.info("✅ Agent 循环模式已停止")
  1783. # ============================================================================
  1784. # 交互式菜单
  1785. # ============================================================================
  1786. def show_interactive_menu() -> None:
  1787. """显示交互式菜单并执行用户选择的操作"""
  1788. global ENABLE_AUTO_DEPLOY
  1789. while True:
  1790. print("\n" + "=" * 60)
  1791. print("自动任务执行调度脚本 - Agent 模式")
  1792. print("=" * 60)
  1793. print("\n请选择操作模式:\n")
  1794. print(" 1. Agent 单次执行")
  1795. print(" 2. Agent 循环模式(含自动部署脚本和n8n工作流)")
  1796. print(" 3. Agent 循环模式(禁用部署)")
  1797. print(" 4. 测试生产服务器连接")
  1798. print(" 5. 查看当前任务状态")
  1799. print(" 6. 手动触发任务部署")
  1800. print(" 7. 强制关闭所有 Agent")
  1801. print(" 0. 退出")
  1802. print("\n" + "-" * 60)
  1803. try:
  1804. choice = input("请输入选项 [0-5]: ").strip()
  1805. except (KeyboardInterrupt, EOFError):
  1806. print("\n再见!")
  1807. break
  1808. if choice == "0":
  1809. print("再见!")
  1810. break
  1811. elif choice == "1":
  1812. print("\n启动 Agent 单次执行模式...")
  1813. run_agent_once(timeout=3600, auto_close=True)
  1814. input("\n按 Enter 键返回菜单...")
  1815. elif choice == "2":
  1816. try:
  1817. interval_str = input("请输入检查间隔(秒,默认300): ").strip()
  1818. interval = int(interval_str) if interval_str else 300
  1819. except ValueError:
  1820. interval = 300
  1821. print("\n🚀 启动 Agent 循环模式(含自动部署)")
  1822. print(f" 检查间隔: {interval} 秒")
  1823. print(" 自动部署: ✅ 已启用")
  1824. print("\n 任务完成后将自动:")
  1825. print(" - 部署 Python 脚本到生产服务器")
  1826. print(" - 查找并部署相关 n8n 工作流")
  1827. print("\n按 Ctrl+C 停止服务并返回菜单\n")
  1828. ENABLE_AUTO_DEPLOY = True
  1829. try:
  1830. run_agent_loop(interval=interval)
  1831. except KeyboardInterrupt:
  1832. print("\n循环已停止")
  1833. elif choice == "3":
  1834. try:
  1835. interval_str = input("请输入检查间隔(秒,默认300): ").strip()
  1836. interval = int(interval_str) if interval_str else 300
  1837. except ValueError:
  1838. interval = 300
  1839. print(f"\n启动 Agent 循环模式(禁用部署),检查间隔: {interval} 秒")
  1840. print("按 Ctrl+C 停止服务并返回菜单\n")
  1841. ENABLE_AUTO_DEPLOY = False
  1842. try:
  1843. run_agent_loop(interval=interval)
  1844. except KeyboardInterrupt:
  1845. print("\n循环已停止")
  1846. elif choice == "4":
  1847. print("\n测试生产服务器连接...")
  1848. if test_ssh_connection():
  1849. print("✅ 连接测试成功")
  1850. else:
  1851. print("❌ 连接测试失败")
  1852. input("\n按 Enter 键返回菜单...")
  1853. elif choice == "5":
  1854. print("\n当前任务状态:")
  1855. print("-" * 40)
  1856. # 从数据库获取 pending 任务
  1857. pending_tasks = get_pending_tasks()
  1858. print(f" 数据库中 pending 任务: {len(pending_tasks)} 个")
  1859. for task in pending_tasks:
  1860. print(f" - [{task['task_id']}] {task['task_name']}")
  1861. # 从本地文件获取 processing 任务
  1862. processing_ids = get_processing_task_ids()
  1863. print(f" 本地 processing 任务: {len(processing_ids)} 个")
  1864. if processing_ids:
  1865. print(f" 任务 ID: {processing_ids}")
  1866. # 显示已完成的任务
  1867. if PENDING_TASKS_FILE.exists():
  1868. try:
  1869. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  1870. all_local_tasks = json.load(f)
  1871. completed_tasks = [
  1872. t for t in all_local_tasks if t.get("status") == "completed"
  1873. ]
  1874. print(f" 本地 completed 任务: {len(completed_tasks)} 个")
  1875. for task in completed_tasks:
  1876. print(
  1877. f" - [{task.get('task_id')}] {task.get('task_name')} -> {task.get('code_file', 'N/A')}"
  1878. )
  1879. except Exception:
  1880. pass
  1881. input("\n按 Enter 键返回菜单...")
  1882. elif choice == "6":
  1883. print("\n手动触发任务部署")
  1884. print("-" * 40)
  1885. # 显示已完成的任务列表
  1886. if PENDING_TASKS_FILE.exists():
  1887. try:
  1888. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  1889. all_tasks = json.load(f)
  1890. completed_tasks = [
  1891. t for t in all_tasks if t.get("status") == "completed"
  1892. ]
  1893. if not completed_tasks:
  1894. print("没有已完成的任务可供部署")
  1895. input("\n按 Enter 键返回菜单...")
  1896. continue
  1897. print("已完成的任务:")
  1898. for idx, task in enumerate(completed_tasks, 1):
  1899. task_id = task.get("task_id", "N/A")
  1900. task_name = task.get("task_name", "未知")
  1901. code_file = task.get("code_file", "N/A")
  1902. print(f" {idx}. [{task_id}] {task_name}")
  1903. print(f" 代码文件: {code_file}")
  1904. print("\n 0. 部署全部")
  1905. print(" q. 返回菜单")
  1906. try:
  1907. selection = input("\n请选择要部署的任务编号: ").strip().lower()
  1908. if selection == "q":
  1909. continue
  1910. tasks_to_deploy = []
  1911. if selection == "0":
  1912. tasks_to_deploy = completed_tasks
  1913. else:
  1914. try:
  1915. idx = int(selection) - 1
  1916. if 0 <= idx < len(completed_tasks):
  1917. tasks_to_deploy = [completed_tasks[idx]]
  1918. else:
  1919. print("❌ 无效的编号")
  1920. continue
  1921. except ValueError:
  1922. print("❌ 请输入有效的数字")
  1923. continue
  1924. if tasks_to_deploy:
  1925. print(f"\n🚀 开始部署 {len(tasks_to_deploy)} 个任务...")
  1926. ENABLE_AUTO_DEPLOY = True
  1927. success_count = 0
  1928. for task in tasks_to_deploy:
  1929. if auto_deploy_completed_task(task):
  1930. success_count += 1
  1931. print(
  1932. f"\n📊 部署完成: {success_count}/{len(tasks_to_deploy)} 成功"
  1933. )
  1934. except (KeyboardInterrupt, EOFError):
  1935. pass
  1936. except Exception as e:
  1937. print(f"❌ 读取任务列表失败: {e}")
  1938. else:
  1939. print("没有本地任务记录")
  1940. input("\n按 Enter 键返回菜单...")
  1941. elif choice == "7":
  1942. print("\n🔄 强制关闭所有 Agent 会话...")
  1943. if HAS_CURSOR_GUI:
  1944. if force_close_all_agents():
  1945. print("✅ 所有 Agent 会话已关闭")
  1946. else:
  1947. print("⚠️ 关闭过程中可能出现问题,请检查 Cursor 窗口")
  1948. else:
  1949. print("❌ 当前环境不支持 GUI 自动化")
  1950. input("\n按 Enter 键返回菜单...")
  1951. else:
  1952. print("❌ 无效的选项,请重新选择")
  1953. # ============================================================================
  1954. # 主函数
  1955. # ============================================================================
  1956. def main() -> None:
  1957. """主函数"""
  1958. parser = argparse.ArgumentParser(
  1959. description="自动任务执行调度脚本 (Agent 模式)",
  1960. formatter_class=argparse.RawDescriptionHelpFormatter,
  1961. epilog="""
  1962. 示例:
  1963. # Agent 单次执行
  1964. python scripts/auto_execute_tasks.py --agent-run
  1965. # Agent 循环模式
  1966. python scripts/auto_execute_tasks.py --agent-loop
  1967. # Agent 循环模式 + 禁用自动部署
  1968. python scripts/auto_execute_tasks.py --agent-loop --no-deploy
  1969. # 设置 Agent 超时时间
  1970. python scripts/auto_execute_tasks.py --agent-run --agent-timeout 7200
  1971. # 立即部署指定任务到生产服务器
  1972. python scripts/auto_execute_tasks.py --deploy-now 123
  1973. # 测试生产服务器连接
  1974. python scripts/auto_execute_tasks.py --test-connection
  1975. """,
  1976. )
  1977. # Agent 模式参数
  1978. parser.add_argument(
  1979. "--agent-run",
  1980. action="store_true",
  1981. help="Agent 单次执行模式",
  1982. )
  1983. parser.add_argument(
  1984. "--agent-loop",
  1985. action="store_true",
  1986. help="Agent 循环模式",
  1987. )
  1988. parser.add_argument(
  1989. "--agent-timeout",
  1990. type=int,
  1991. default=3600,
  1992. help="Agent 等待任务完成的超时时间(秒),默认 3600",
  1993. )
  1994. parser.add_argument(
  1995. "--interval",
  1996. type=int,
  1997. default=300,
  1998. help="循环模式检查间隔(秒),默认 300",
  1999. )
  2000. parser.add_argument(
  2001. "--no-auto-close",
  2002. action="store_true",
  2003. help="任务完成后不自动关闭 Agent",
  2004. )
  2005. # 部署相关参数
  2006. parser.add_argument(
  2007. "--no-deploy",
  2008. action="store_true",
  2009. help="禁用自动部署功能",
  2010. )
  2011. parser.add_argument(
  2012. "--deploy-now",
  2013. type=str,
  2014. metavar="TASK_ID",
  2015. help="立即部署指定任务ID的脚本到生产服务器",
  2016. )
  2017. parser.add_argument(
  2018. "--test-connection",
  2019. action="store_true",
  2020. help="测试到生产服务器的 SSH 连接",
  2021. )
  2022. args = parser.parse_args()
  2023. global ENABLE_AUTO_DEPLOY
  2024. ENABLE_AUTO_DEPLOY = not args.no_deploy
  2025. auto_close = not args.no_auto_close
  2026. # 测试 SSH 连接
  2027. if args.test_connection:
  2028. if test_ssh_connection():
  2029. logger.info("✅ 连接测试成功")
  2030. else:
  2031. logger.error("❌ 连接测试失败")
  2032. return
  2033. # 立即部署指定任务
  2034. if args.deploy_now:
  2035. try:
  2036. task_id = int(args.deploy_now)
  2037. logger.info(f"开始部署任务 {task_id}...")
  2038. # 从 pending_tasks.json 查找任务信息
  2039. if PENDING_TASKS_FILE.exists():
  2040. with PENDING_TASKS_FILE.open("r", encoding="utf-8") as f:
  2041. tasks = json.load(f)
  2042. task_found = None
  2043. for t in tasks:
  2044. if t.get("task_id") == task_id:
  2045. task_found = t
  2046. break
  2047. if task_found:
  2048. if auto_deploy_completed_task(task_found):
  2049. logger.info(f"✅ 任务 {task_id} 部署成功")
  2050. else:
  2051. logger.error(f"❌ 任务 {task_id} 部署失败")
  2052. else:
  2053. logger.error(f"未找到任务 {task_id}")
  2054. else:
  2055. logger.error("pending_tasks.json 文件不存在")
  2056. except ValueError:
  2057. logger.error(f"无效的任务ID: {args.deploy_now}")
  2058. return
  2059. # Agent 单次执行
  2060. if args.agent_run:
  2061. success = run_agent_once(
  2062. timeout=args.agent_timeout,
  2063. auto_close=auto_close,
  2064. )
  2065. if success:
  2066. logger.info("✅ Agent 单次执行完成")
  2067. else:
  2068. logger.error("❌ Agent 单次执行失败")
  2069. return
  2070. # Agent 循环模式
  2071. if args.agent_loop:
  2072. run_agent_loop(
  2073. interval=args.interval,
  2074. timeout=args.agent_timeout,
  2075. auto_close=auto_close,
  2076. )
  2077. return
  2078. # 没有指定任何模式参数时,显示交互式菜单
  2079. if len(sys.argv) == 1:
  2080. show_interactive_menu()
  2081. else:
  2082. # 显示帮助信息
  2083. parser.print_help()
  2084. if __name__ == "__main__":
  2085. main()