BIZ-38: CacheManager + CoordinatedPoller + multica_proxy — 共享心跳脚本v1.0
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user