Files
EnterpriseArchitect/shared-scripts/heartbeat_helper.py
T

475 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
heartbeat_helper.py — 高频 Agent 心跳辅助脚本
提供心跳脚本中所有通用功能,底层通过 multica_proxy 调用 multica CLI
自动享受缓存和限流保护。
用法:
from heartbeat_helper import check_my_tasks, check_timeouts, check_dependencies
作者:陆怀瑾(COO
日期:2026-06-23
"""
import os
import sys
import json
import time
from typing import Any, Dict, List, Optional
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if _SCRIPT_DIR not in sys.path:
sys.path.insert(0, _SCRIPT_DIR)
from multica_proxy import (
run_multica,
multica_issue_list_my_todo,
multica_issue_list_in_progress,
multica_issue_get,
openclaw_workboard_list,
openclaw_workboard_read,
get_cache_stats,
clear_cache,
start_coordinated_poller,
subscribe_to_poller,
get_poller_status,
health_check,
)
# ============================================================================
# Agent 配置
# ============================================================================
AGENT_CONFIGS = {
"coo": {
"name": "陆怀瑾",
"multica_uuid": "1c38b437-b54d-4784-bda3-29ce4c8a6722",
"openclaw_agent_id": "coo",
"is_coo": True,
},
"secretary": {
"name": "刘诗妮",
"multica_uuid": "b024fcdc-30ff-420d-b289-498041466e1b",
"openclaw_agent_id": "secretary",
"is_coo": False,
},
"projectmanager": {
"name": "胡蓉",
"multica_uuid": "d877b8c3-b230-4073-b3f7-80e148cfdb71",
"openclaw_agent_id": "projectmanager",
"is_coo": False,
},
"costcodev": {
"name": "徐聪",
"multica_uuid": "46bdd4a6-5c64-475a-92ef-36a763602fa1",
"openclaw_agent_id": "costcodev",
"is_coo": False,
},
"opengineer": {
"name": "严维序",
"multica_uuid": "d3804433-9e2e-4199-a92b-a153049b3bc9",
"openclaw_agent_id": "opengineer",
"is_coo": False,
},
"productmanager": {
"name": "沈路明",
"multica_uuid": "a101fa88-d821-4839-9754-e04580d5fd68",
"openclaw_agent_id": "productmanager",
"is_coo": False,
},
"architect": {
"name": "梁思筑",
"multica_uuid": "40abd41a-62d0-416d-bc44-92c1f758d87a",
"openclaw_agent_id": "architect",
"is_coo": False,
},
"designer": {
"name": "苏锦绘",
"multica_uuid": "13bd8968-cc2a-4934-90c7-957a2d3c09c2",
"openclaw_agent_id": "designer",
"is_coo": False,
},
"contentspecialist": {
"name": "文墨言",
"multica_uuid": "8321b0bf-7d89-4ece-927a-0780f42ad396",
"openclaw_agent_id": "contentspecialist",
"is_coo": False,
},
"cvexpert": {
"name": "程伯予",
"multica_uuid": "4a8696fd-6531-40da-8956-ef84d7ea3c43",
"openclaw_agent_id": "cvexpert",
"is_coo": False,
},
"prompt-engineer": {
"name": "许言",
"multica_uuid": "ece81d8e-8a24-4dd8-a7af-8adfc54b9d01",
"openclaw_agent_id": "prompt-engineer",
"is_coo": False,
},
"mediaspecialist": {
"name": "钟帧韵",
"multica_uuid": "e2b587d4-1d16-447c-8ad9-e2a01358ff0a",
"openclaw_agent_id": "mediaspecialist",
"is_coo": False,
},
"taobaospecialist": {
"name": "陆云帆",
"multica_uuid": "e0f62d8f-9568-4f41-8ad4-b73d79a163a7",
"openclaw_agent_id": "taobaospecialist",
"is_coo": False,
},
"marketanalysis": {
"name": "顾析策",
"multica_uuid": "5ed91729-658f-4654-98f0-3e0313022002",
"openclaw_agent_id": "marketanalysis",
"is_coo": False,
},
"lawyer": {
"name": "苏慎",
"multica_uuid": "6fb0fbd2-16a6-4566-ba7a-d2c136baec25",
"openclaw_agent_id": "lawyer",
"is_coo": False,
},
}
def get_agent_config(agent_id: str) -> Dict[str, Any]:
"""获取 Agent 配置"""
config = AGENT_CONFIGS.get(agent_id)
if config is None:
raise ValueError(f"Unknown agent: {agent_id}. Known: {list(AGENT_CONFIGS.keys())}")
return config
# ============================================================================
# 三源任务检查
# ============================================================================
def check_workboard_tasks(agent_id: str) -> List[Dict[str, Any]]:
"""
检查 WorkBoard 中分配给当前 Agent 的待办卡片
替代内联 bash 脚本
"""
result = openclaw_workboard_list()
if not result["success"]:
print(f"[heartbeat] WorkBoard 查询失败: {result['error']}")
return []
data = result["data"]
my_cards = [
c for c in data.get("cards", [])
if c.get("agentId") == agent_id and c.get("status") == "todo"
]
return my_cards
def check_multica_tasks(agent_id: str) -> List[Dict[str, Any]]:
"""
检查 Multica 中分配给当前 Agent 的待办 Issue
替代内联 bash 脚本
"""
config = get_agent_config(agent_id)
result = multica_issue_list_my_todo(config["multica_uuid"])
if not result["success"]:
print(f"[heartbeat] Multica 查询失败: {result['error']}")
return []
data = result["data"]
if isinstance(data, list):
return data
return []
def check_todo_docs(workspace_dir: str) -> List[str]:
"""
检查工作区待办文档中的未完成项
"""
items = []
for filename in ["TODO.md", "AGENTS.md"]:
filepath = os.path.join(workspace_dir, filename)
if os.path.exists(filepath):
try:
with open(filepath) as f:
for i, line in enumerate(f, 1):
if "[ ]" in line:
items.append(f"{filename}:{i}: {line.strip()}")
except Exception:
pass
return items
def check_my_tasks(agent_id: str, workspace_dir: str) -> Dict[str, Any]:
"""
三源合并检查:WorkBoard + Multica + 待办文档
"""
wb_tasks = check_workboard_tasks(agent_id)
mul_tasks = check_multica_tasks(agent_id)
doc_tasks = check_todo_docs(workspace_dir)
return {
"workboard": wb_tasks,
"multica": mul_tasks,
"documents": doc_tasks,
"total": len(wb_tasks) + len(mul_tasks) + len(doc_tasks),
}
# ============================================================================
# 超时检测
# ============================================================================
TIMEOUT_SECONDS = 1200 # 20 分钟
def check_workboard_timeouts() -> List[Dict[str, Any]]:
"""
检查 WorkBoard 中超过 20 分钟无进展的进行中任务
"""
result = openclaw_workboard_list()
if not result["success"]:
print(f"[heartbeat] WorkBoard 超时检测失败: {result['error']}")
return []
data = result["data"]
now = time.time()
timeouts = []
for c in data.get("cards", []):
if c.get("status") != "in_progress":
continue
updated = c.get("updated_at", "")
if updated:
try:
age = now - time.mktime(time.strptime(updated[:19], "%Y-%m-%dT%H:%M:%S"))
if age > TIMEOUT_SECONDS:
timeouts.append(c)
except (ValueError, OverflowError):
pass
return timeouts
def check_multica_timeouts() -> List[Dict[str, Any]]:
"""
检查 Multica 中超过 20 分钟无进展的进行中 Issue
"""
result = multica_issue_list_in_progress()
if not result["success"]:
print(f"[heartbeat] Multica 超时检测失败: {result['error']}")
return []
data = result["data"]
now = time.time()
timeouts = []
if isinstance(data, list):
for issue in data:
updated = issue.get("updated_at", "")
if updated:
try:
age = now - time.mktime(time.strptime(updated[:19], "%Y-%m-%dT%H:%M:%S"))
if age > TIMEOUT_SECONDS:
timeouts.append(issue)
except (ValueError, OverflowError):
pass
return timeouts
def check_timeouts() -> Dict[str, Any]:
"""
跨平台超时检测
"""
wb_timeouts = check_workboard_timeouts()
mul_timeouts = check_multica_timeouts()
return {
"workboard_timeouts": wb_timeouts,
"multica_timeouts": mul_timeouts,
"total_timeouts": len(wb_timeouts) + len(mul_timeouts),
}
# ============================================================================
# 依赖检查
# ============================================================================
def check_workboard_dependencies(card_id: str) -> Dict[str, Any]:
"""
检查 WorkBoard 卡片的依赖是否满足
"""
result = openclaw_workboard_read(card_id)
if not result["success"]:
return {"satisfied": False, "error": result["error"], "unmet": []}
card = result["data"]
deps = card.get("dependsOn", [])
unmet = [dep for dep in deps if dep.get("status") != "done"]
return {
"satisfied": len(unmet) == 0,
"total_deps": len(deps),
"unmet": unmet,
}
def check_multica_dependencies(issue_id: str) -> Dict[str, Any]:
"""
检查 Multica Issue 的父 Issue 依赖是否满足
"""
result = multica_issue_get(issue_id)
if not result["success"]:
return {"satisfied": False, "error": result["error"], "unmet": []}
issue = result["data"]
parent_id = issue.get("parent_issue_id")
if not parent_id:
return {"satisfied": True, "total_deps": 0, "unmet": []}
parent_result = multica_issue_get(parent_id)
if not parent_result["success"]:
return {"satisfied": False, "error": f"Failed to check parent {parent_id}", "unmet": [parent_id]}
parent = parent_result["data"]
if parent.get("status") != "done":
return {"satisfied": False, "total_deps": 1, "unmet": [{"id": parent_id, "identifier": parent.get("identifier"), "status": parent.get("status")}]}
return {"satisfied": True, "total_deps": 1, "unmet": []}
# ============================================================================
# 全局积压巡检(COO 专用)
# ============================================================================
def check_global_backlog() -> Dict[str, Any]:
"""
全平台积压巡检:WorkBoard + Multica 全局待办数
"""
wb_result = openclaw_workboard_list()
mul_result = multica_issue_list_in_progress()
wb_stats = {"total": 0, "todo": 0, "in_progress": 0, "done": 0}
if wb_result["success"]:
cards = wb_result["data"].get("cards", [])
wb_stats["total"] = len(cards)
for c in cards:
status = c.get("status", "")
if status in wb_stats:
wb_stats[status] += 1
mul_stats = {"total": 0, "in_progress": 0}
if mul_result["success"] and isinstance(mul_result["data"], list):
mul_stats["total"] = len(mul_result["data"])
mul_stats["in_progress"] = mul_stats["total"]
return {
"workboard": wb_stats,
"multica": mul_stats,
}
# ============================================================================
# 心跳主入口
# ============================================================================
def run_heartbeat(agent_id: str, workspace_dir: str) -> Dict[str, Any]:
"""
执行完整心跳检查
参数:
agent_id: Agent ID(如 "coo", "secretary"
workspace_dir: 工作区目录路径
返回:
心跳结果字典
"""
config = get_agent_config(agent_id)
is_coo = config["is_coo"]
result = {
"agent": config["name"],
"agent_id": agent_id,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
"tasks": check_my_tasks(agent_id, workspace_dir),
"timeouts": check_timeouts(),
}
# COO 额外检查
if is_coo:
result["global_backlog"] = check_global_backlog()
result["cache_stats"] = get_cache_stats()
result["poller_status"] = get_poller_status()
return result
def print_heartbeat_report(result: Dict[str, Any]) -> None:
"""打印格式化的心跳报告"""
print(f"\n{'='*60}")
print(f" 🫀 心跳报告 — {result['agent']} ({result['agent_id']})")
print(f" ⏰ {result['timestamp']}")
print(f"{'='*60}")
tasks = result["tasks"]
print(f"\n📋 任务检查:")
print(f" WorkBoard 待办: {len(tasks['workboard'])}")
for t in tasks["workboard"]:
print(f" ⚠️ WB TODO: {t['id'][:8]}{t.get('agentId','?')} - {t.get('title','?')[:50]}")
print(f" Multica 待办: {len(tasks['multica'])}")
for t in tasks["multica"]:
print(f" ⚠️ MUL TODO: {t.get('identifier','?')} - {t.get('title','?')[:50]}")
print(f" 文档待办: {len(tasks['documents'])}")
for d in tasks["documents"]:
print(f" 📝 {d}")
timeouts = result["timeouts"]
print(f"\n⏱️ 超时检测:")
print(f" WorkBoard 超时: {len(timeouts['workboard_timeouts'])}")
for t in timeouts["workboard_timeouts"]:
print(f" ⏰ WB TIMEOUT: {t['id'][:8]} [{t.get('agentId','?')}] {t.get('title','?')[:50]}")
print(f" Multica 超时: {len(timeouts['multica_timeouts'])}")
for t in timeouts["multica_timeouts"]:
print(f" ⏰ MUL TIMEOUT: {t.get('identifier','?')} {t.get('title','?')[:50]}")
if "global_backlog" in result:
gb = result["global_backlog"]
print(f"\n📊 全局积压:")
print(f" WorkBoard: {gb['workboard']}")
print(f" Multica: {gb['multica']}")
if "cache_stats" in result:
print(f"\n💾 缓存: {result['cache_stats']}")
print(f"\n{'='*60}\n")
# ============================================================================
# CLI 入口
# ============================================================================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Agent 心跳辅助脚本")
parser.add_argument("agent_id", help="Agent ID (coo/secretary/projectmanager/costcodev/opengineer)")
parser.add_argument("--workspace", "-w", default=os.getcwd(), help="工作区目录")
parser.add_argument("--json", action="store_true", help="JSON 输出")
parser.add_argument("--health", action="store_true", help="健康检查")
parser.add_argument("--clear-cache", action="store_true", help="清理缓存")
args = parser.parse_args()
if args.health:
print(json.dumps(health_check(), indent=2, ensure_ascii=False))
elif args.clear_cache:
count = clear_cache()
print(f"已清理 {count} 条缓存")
else:
result = run_heartbeat(args.agent_id, args.workspace)
if args.json:
print(json.dumps(result, indent=2, ensure_ascii=False, default=str))
else:
print_heartbeat_report(result)