BIZ-38: CacheManager + CoordinatedPoller + multica_proxy — 共享心跳脚本v1.0

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-06-24 11:23:05 +08:00
parent 6b5f53a0fd
commit 93e8a1011b
3 changed files with 1555 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
"""
multica_proxy.py — multica CLI 调用代理
封装 multica CLI 调用,自动带缓存和限流保护。
各 Agent 心跳脚本中用 multica_proxy 替代直接 subprocess.run(["multica",...])
依赖:rate_limiter.pyCacheManager, RequestScheduler, CoordinatedPoller
作者:陆怀瑾(COO
日期:2026-06-23
"""
import os
import sys
import json
import subprocess
import hashlib
from typing import Any, Dict, Optional
# 确保能找到 rate_limiter
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if _SCRIPT_DIR not in sys.path:
sys.path.insert(0, _SCRIPT_DIR)
from rate_limiter import CacheManager, RequestScheduler, CoordinatedPoller, Priority
# ============================================================================
# 全局单例
# ============================================================================
_cache = CacheManager()
_scheduler: Optional[RequestScheduler] = None
_poller: Optional[CoordinatedPoller] = None
def _get_scheduler() -> RequestScheduler:
"""获取或创建调度器单例"""
global _scheduler
if _scheduler is None:
_scheduler = RequestScheduler(rate=40/60, capacity=40, enable_cache=True)
_scheduler.start()
return _scheduler
def _get_poller() -> CoordinatedPoller:
"""获取或创建统一轮询器单例"""
global _poller
if _poller is None:
_poller = CoordinatedPoller(_get_scheduler(), poll_interval=15*60)
return _poller
# ============================================================================
# 缓存查询辅助
# ============================================================================
def _make_cache_key(cmd: list) -> str:
"""为 CLI 命令生成缓存键"""
return hashlib.md5(json.dumps(cmd, sort_keys=True).encode()).hexdigest()
def _cache_category(cmd: list) -> str:
"""根据命令推断缓存类别"""
cmd_str = " ".join(str(x) for x in cmd)
if "workboard" in cmd_str:
return "workboard"
if "config" in cmd_str or "agent" in cmd_str:
return "config"
if "wiki" in cmd_str or "knowledge" in cmd_str:
return "knowledge"
if "user" in cmd_str or "member" in cmd_str:
return "user"
return "workboard" # 默认 5 分钟
# ============================================================================
# 核心代理函数
# ============================================================================
# OpenClaw 工作区 ID(全局常量)
# 用于所有 multica CLI 调用,确保隔离会话也能正确查询
_WORKSPACE_ID = "54344e11-6bb2-4d95-a5e5-c8b075a07cea"
def _inject_workspace_id(cmd: list) -> list:
"""自动注入 workspace-id 到 multica CLI 命令"""
if len(cmd) >= 2 and cmd[0] == "multica" and "--workspace-id" not in cmd:
# 插入在命令和子命令之后、标志之前
insert_idx = 1
while insert_idx < len(cmd) and not cmd[insert_idx].startswith("--"):
insert_idx += 1
new_cmd = cmd[:insert_idx] + ["--workspace-id", _WORKSPACE_ID] + cmd[insert_idx:]
return new_cmd
return cmd
def run_multica(cmd: list, use_cache: bool = True, timeout: int = 30) -> Dict[str, Any]:
"""
执行 multica CLI 命令(带缓存和限流)
参数:
cmd: 命令列表,如 ["multica", "issue", "list", "--output", "json"]
use_cache: 是否使用缓存
timeout: 超时时间(秒)
返回:
{"success": bool, "data": Any, "from_cache": bool, "error": str|None}
"""
# 自动注入 workspace-id,确保隔离会话正确查询
cmd = _inject_workspace_id(cmd)
category = _cache_category(cmd)
# 1. 尝试从缓存获取
if use_cache:
cached = _cache.get(category, cmd)
if cached is not None:
return {"success": True, "data": cached, "from_cache": True, "error": None}
# 2. 执行 CLI 命令
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout
)
if result.returncode != 0:
error_msg = result.stderr.strip() or f"Exit code {result.returncode}"
return {"success": False, "data": None, "from_cache": False, "error": error_msg}
# 尝试解析 JSON
try:
data = json.loads(result.stdout)
except json.JSONDecodeError:
data = result.stdout.strip()
# 3. 写入缓存
if use_cache:
_cache.set(category, cmd, data)
return {"success": True, "data": data, "from_cache": False, "error": None}
except subprocess.TimeoutExpired:
return {"success": False, "data": None, "from_cache": False, "error": f"Command timed out after {timeout}s"}
except Exception as e:
return {"success": False, "data": None, "from_cache": False, "error": str(e)}
def run_openclaw_workboard(cmd: list, use_cache: bool = True, timeout: int = 30) -> Dict[str, Any]:
"""
执行 openclaw workboard CLI 命令(带缓存)
参数同 run_multica
"""
return run_multica(cmd, use_cache=use_cache, timeout=timeout)
# ============================================================================
# 便捷函数:心跳脚本中直接替换
# ============================================================================
def multica_issue_list_my_todo(assignee_id: str) -> Dict[str, Any]:
"""
获取分配给我的待办 Issue 列表
替代: multica issue list --assignee-id <id> --status todo --output json
"""
return run_multica([
"multica", "issue", "list",
"--assignee-id", assignee_id,
"--status", "todo",
"--output", "json"
])
def multica_issue_list_in_progress() -> Dict[str, Any]:
"""
获取所有进行中的 Issue 列表(超时检测用)
替代: multica issue list --status in_progress --output json
"""
return run_multica([
"multica", "issue", "list",
"--status", "in_progress",
"--output", "json"
])
def multica_issue_get(issue_id: str) -> Dict[str, Any]:
"""
获取单个 Issue 详情
替代: multica issue get <id> --output json
"""
return run_multica([
"multica", "issue", "get",
issue_id,
"--output", "json"
])
def openclaw_workboard_list() -> Dict[str, Any]:
"""
获取 WorkBoard 卡片列表
替代: openclaw workboard list --json
"""
return run_multica([
"openclaw", "workboard", "list", "--json"
])
def openclaw_workboard_read(card_id: str) -> Dict[str, Any]:
"""
获取单个 WorkBoard 卡片
替代: openclaw workboard read <id> --json
"""
return run_multica([
"openclaw", "workboard", "read", card_id, "--json"
])
# ============================================================================
# 缓存管理
# ============================================================================
def get_cache_stats() -> Dict[str, Any]:
"""获取缓存统计"""
return _cache.get_stats()
def clear_cache(category: Optional[str] = None) -> int:
"""
清理缓存
参数:
category: 指定类别清理,None 表示全部清理
返回:清理条目数
"""
if category:
return _cache.clear_expired()
else:
count = len(_cache._cache)
_cache.clear()
return count
# ============================================================================
# 统一轮询器(仅 COO 使用)
# ============================================================================
def start_coordinated_poller() -> CoordinatedPoller:
"""
启动 COO 统一轮询器
仅 COO Agent 调用此函数
"""
poller = _get_poller()
if not poller._running:
poller.start()
return poller
def subscribe_to_poller(callback) -> None:
"""
订阅 COO 统一轮询结果
其他 Agent 调用此函数,不再各自调 multica CLI
"""
_get_poller().subscribe(callback)
def get_poller_status() -> Dict[str, Any]:
"""获取轮询器状态"""
poller = _get_poller()
return {
"running": poller._running,
"poll_interval": poller.poll_interval,
"subscriber_count": len(poller._subscribers)
}
# ============================================================================
# 健康检查
# ============================================================================
def health_check() -> Dict[str, Any]:
"""检查 multica_proxy 健康状态"""
scheduler = _get_scheduler()
return {
"status": "ok",
"cache": get_cache_stats(),
"scheduler": scheduler.get_status(),
"poller": get_poller_status()
}
# ============================================================================
# 测试
# ============================================================================
if __name__ == "__main__":
print("=== multica_proxy 健康检查 ===")
print(json.dumps(health_check(), indent=2, ensure_ascii=False))
print("\n=== 测试缓存 ===")
# 第一次调用(无缓存)
result1 = run_multica(["echo", "test1"], use_cache=True)
print(f"第1次: from_cache={result1['from_cache']}")
# 第二次调用(应命中缓存)
result2 = run_multica(["echo", "test1"], use_cache=True)
print(f"第2次: from_cache={result2['from_cache']}")
print("\n测试完成")