93e8a1011b
Co-authored-by: multica-agent <github@multica.ai>
310 lines
9.2 KiB
Python
310 lines
9.2 KiB
Python
"""
|
||
multica_proxy.py — multica CLI 调用代理
|
||
|
||
封装 multica CLI 调用,自动带缓存和限流保护。
|
||
各 Agent 心跳脚本中用 multica_proxy 替代直接 subprocess.run(["multica",...])
|
||
|
||
依赖:rate_limiter.py(CacheManager, 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测试完成")
|