a55d241f30
- 创建 mofin.db → todos 表(id/title/status/priority/fix_action/retry机制) - 创建 self_todo_executor.py:no_agent脚本,纯代码逻辑 - 有fix_action→执行→验证→标记completed - 无fix_action→标记blocked→输出通知 - 失败重试3次→超限标blocked - 新blocked项首次输出后缓存不重复 - 修改 morning_health_check: - TODO写入DB取代JSON(sqlite3,无row_factory依赖) - 去重:含completed查重 - 输出阻塞TODO列表 - 替换cron:LLM cron(2h间距) → no_agent(10min间距) - 修复:deliver=origin两任务改为local - 清理:废弃todo.json退役
181 lines
6.4 KiB
Python
181 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""self_todo_executor.py — TODO自动执行器 (no_agent模式)
|
|
|
|
每10分钟轮询mofin.db中todos表的pending任务,执行fix_action命令。
|
|
纯代码逻辑,不调LLM。
|
|
有执行→输出摘要,无→SILENT。
|
|
"""
|
|
|
|
import json, os, sqlite3, subprocess, sys, time
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
BASE = Path("/home/hmo/MoFin")
|
|
DB_PATH = BASE / "data" / "mofin.db"
|
|
# 记录已报告过的blocked ID,避免重复推送
|
|
REPORTED_BLOCKED_PATH = Path("/home/hmo/.hermes/profiles/position-analyst/home/.cache/executor_reported_blocked.json")
|
|
|
|
|
|
def get_conn():
|
|
conn = sqlite3.connect(str(DB_PATH))
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def verify_fix(verification_check):
|
|
"""运行验证命令,返回 (ok, output)"""
|
|
if not verification_check:
|
|
return True, "无验证命令"
|
|
try:
|
|
r = subprocess.run(
|
|
verification_check,
|
|
shell=True, capture_output=True, text=True, timeout=30
|
|
)
|
|
if r.returncode == 0:
|
|
return True, r.stdout.strip()[:200]
|
|
else:
|
|
return False, f"exit={r.returncode}: {r.stderr.strip()[:200]}"
|
|
except Exception as e:
|
|
return False, str(e)[:200]
|
|
|
|
|
|
def execute_fix(fix_action):
|
|
"""执行修复命令,返回 (ok, output)"""
|
|
if not fix_action:
|
|
return False, "无修复命令"
|
|
try:
|
|
r = subprocess.run(
|
|
fix_action,
|
|
shell=True, capture_output=True, text=True, timeout=60
|
|
)
|
|
if r.returncode == 0:
|
|
return True, r.stdout.strip()[:300] or "ok"
|
|
else:
|
|
return False, f"exit={r.returncode}: {r.stderr.strip()[:300]}"
|
|
except subprocess.TimeoutExpired:
|
|
return False, "执行超时(60s)"
|
|
except Exception as e:
|
|
return False, str(e)[:200]
|
|
|
|
|
|
def main():
|
|
start = time.time()
|
|
conn = get_conn()
|
|
cur = conn.cursor()
|
|
|
|
# 读所有pending任务(按优先级排序)
|
|
rows = cur.execute(
|
|
"SELECT id, title, description, priority, fix_action, verification_check, "
|
|
"retry_count, max_retries, note FROM todos "
|
|
"WHERE status='pending' ORDER BY "
|
|
"CASE priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, "
|
|
"created_at ASC LIMIT 10"
|
|
).fetchall()
|
|
|
|
if not rows:
|
|
conn.close()
|
|
print("[SILENT] 无待处理TODO")
|
|
return
|
|
|
|
results = []
|
|
newly_blocked = [] # [(id, title)] 新阻塞的,需要推送给Dad/知微
|
|
|
|
for row in rows:
|
|
todo_id = row["id"]
|
|
title = row["title"]
|
|
fix_action = row["fix_action"]
|
|
verification = row["verification_check"]
|
|
retry_count = row["retry_count"]
|
|
max_retries = row["max_retries"]
|
|
|
|
# 标记in_progress
|
|
cur.execute(
|
|
"UPDATE todos SET status='in_progress', updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
|
(todo_id,)
|
|
)
|
|
conn.commit()
|
|
|
|
if fix_action:
|
|
# 执行修复
|
|
ok, output = execute_fix(fix_action)
|
|
if not ok:
|
|
retry_count += 1
|
|
if retry_count >= max_retries:
|
|
cur.execute(
|
|
"UPDATE todos SET status='blocked', retry_count=?, "
|
|
"note=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
|
(retry_count, f"重试{retry_count}次仍失败: {output}", todo_id)
|
|
)
|
|
newly_blocked.append((todo_id, title))
|
|
results.append(("⛔", f"{title}: 已阻塞({output[:60]})"))
|
|
else:
|
|
cur.execute(
|
|
"UPDATE todos SET status='pending', retry_count=?, "
|
|
"note=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
|
(retry_count, f"第{retry_count}次失败: {output}", todo_id)
|
|
)
|
|
results.append(("🔄", f"{title}: 重试{retry_count}/{max_retries}({output[:60]})"))
|
|
conn.commit()
|
|
continue
|
|
|
|
# 修复成功→验证
|
|
v_ok, v_out = verify_fix(verification)
|
|
if v_ok:
|
|
cur.execute(
|
|
"UPDATE todos SET status='completed', note=?, "
|
|
"updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
|
(f"已修复: {output[:200]}", todo_id)
|
|
)
|
|
results.append(("✅", f"{title}: 已修复"))
|
|
else:
|
|
# 修复成功但验证失败→可能验证命令不准,标记需人工
|
|
cur.execute(
|
|
"UPDATE todos SET status='completed', note=?, "
|
|
"updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
|
(f"执行成功但验证异常: {v_out}", todo_id)
|
|
)
|
|
results.append(("⚠️", f"{title}: 已执行但验证异常({v_out[:60]})"))
|
|
else:
|
|
# 无fix_action → 标记为无法自动修复
|
|
cur.execute(
|
|
"UPDATE todos SET status='blocked', note='需人工处理(无修复命令)', "
|
|
"updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
|
(todo_id,)
|
|
)
|
|
newly_blocked.append((todo_id, title))
|
|
results.append(("⛔", f"{title}: 无修复命令,已阻塞"))
|
|
|
|
conn.commit()
|
|
|
|
conn.close()
|
|
|
|
# 记录新报告过的blocked ID
|
|
try:
|
|
REPORTED_BLOCKED_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
reported = json.loads(REPORTED_BLOCKED_PATH.read_text()) if REPORTED_BLOCKED_PATH.exists() else []
|
|
# 过滤出尚未报告过的blocked项
|
|
unreported_blocked = [(tid, t) for tid, t in newly_blocked if tid not in reported]
|
|
if unreported_blocked:
|
|
# 有未报告的阻塞项 → 输出直通知微
|
|
print()
|
|
print("⛔ 需知微处理(已阻塞,无自动修复方案):")
|
|
for tid, t in unreported_blocked:
|
|
print(f" #{tid} {t[:70]}")
|
|
# 标记已报告
|
|
reported.extend([tid for tid, _ in unreported_blocked])
|
|
REPORTED_BLOCKED_PATH.write_text(json.dumps(reported[-200:], ensure_ascii=False))
|
|
except:
|
|
pass
|
|
|
|
elapsed = time.time() - start
|
|
if results:
|
|
print(f"自愈执行器 | {datetime.now().strftime('%H:%M')} | {len(results)}条 ({elapsed:.0f}s)")
|
|
for icon, msg in results:
|
|
print(f" {icon} {msg}")
|
|
else:
|
|
print("[SILENT] 无待处理TODO")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|