diff --git a/backend/app/api/bot_iron_law.py b/backend/app/api/bot_iron_law.py new file mode 100644 index 00000000..0f2ae891 --- /dev/null +++ b/backend/app/api/bot_iron_law.py @@ -0,0 +1,258 @@ +""" +铁律KPI看板API — CMA铁律执行效果数据采集 +提供: 验证次数、通过率、违规数、按Bot分组的通过率 +""" +import os, json, sqlite3, logging +from datetime import datetime, timedelta +from fastapi import APIRouter, Depends, HTTPException, Header +from sqlalchemy.orm import Session +from sqlalchemy import func +from app.database import get_db +from app.models import ActionPlan + +logger = logging.getLogger("cma.iron_law") + +router = APIRouter(prefix="/api/cma/bot", tags=["铁律KPI看板"]) + +# ── BOT API Key 配置(复用bot_bridge的一致鉴权) ── +_BOT_API_KEYS = {} + +def _load_bot_keys(): + global _BOT_API_KEYS + raw = os.getenv("CMA_BOT_API_KEYS", "") + if not raw: + _BOT_API_KEYS = { + "cma-bot-finance-2026": {"role": "finance", "name": "财务BOT"}, + "cma-bot-shop-2026": {"role": "business", "name": "店研学BOT"}, + "cma-bot-admin-2026": {"role": "ceo", "name": "管理BOT"}, + } + else: + try: + _BOT_API_KEYS = json.loads(raw) + except: + _BOT_API_KEYS = {} + +_load_bot_keys() + +def verify_bot_key(x_bot_key: str = Header(None, alias="X-BOT-KEY")): + if not x_bot_key or x_bot_key not in _BOT_API_KEYS: + raise HTTPException(401, "无效的BOT API Key") + bot_info = _BOT_API_KEYS[x_bot_key] + return bot_info + + +# ── 各Hermes Profile state.db 路径 ── +HERMES_HOME = "/root/.hermes/profiles" + +def _list_profile_dbs(): + """列出所有Hermes Profile的state.db路径""" + dbs = [] + if not os.path.isdir(HERMES_HOME): + return dbs + for name in os.listdir(HERMES_HOME): + db_path = os.path.join(HERMES_HOME, name, "state.db") + if os.path.isfile(db_path): + dbs.append((name, db_path)) + return sorted(dbs) + + +def _query_state_db(db_path: str, since_days: int = 30): + """从单个 state.db 查询验证相关消息""" + cutoff = datetime.now() - timedelta(days=since_days) + cutoff_ts = cutoff.timestamp() + + results = { + "total_verify_msgs": 0, + "pass_msgs": 0, + "fail_msgs": 0, + "violation_msgs": 0, + } + + try: + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + c = conn.cursor() + + # 查询含验证/铁律关键词的消息(排除tool_call、校验自身和系统prompt) + c.execute(""" + SELECT content, role FROM messages + WHERE timestamp >= ? + AND (content LIKE '%verify%' + OR content LIKE '%验证%' + OR content LIKE '%铁律%' + OR content LIKE '%validate%') + AND content NOT LIKE '%verify_bot_key%' + AND content NOT LIKE '%X-BOT-KEY%' + AND content NOT LIKE '%system_prompt%' + """, (cutoff_ts,)) + + rows = c.fetchall() + results["total_verify_msgs"] = len(rows) + + for row in rows: + content = row["content"] or "" + role = row["role"] + + # 判断是否通过/成功/完成 + pass_patterns = ["通过", "passed", "success", "✅", "完成", "completed", "验证通过"] + fail_patterns = ["失败", "failed", "error", "❌", "违规", "未通过", "错误", "异常"] + violation_patterns = ["违规", "violation", "拦截", "blocked", "违例"] + + # 助理角色消息:用于判断验证结果 + if role == "assistant": + has_pass = any(p in content for p in pass_patterns) + has_fail = any(p in content for p in fail_patterns) + + if has_pass and not has_fail: + results["pass_msgs"] += 1 + elif has_fail and not has_pass: + results["fail_msgs"] += 1 + elif has_pass and has_fail: + # 混合内容,默认算通过(因为通常有通过+补充说明) + results["pass_msgs"] += 1 + + if any(p in content for p in violation_patterns): + results["violation_msgs"] += 1 + + conn.close() + except Exception as e: + logger.warning(f"查询state.db失败 {db_path}: {e}") + + return results + + +def _query_action_plan_verify(db: Session): + """从CMA ActionPlan表查询验证数据""" + total = db.query(func.count(ActionPlan.id)).scalar() or 0 + verified = db.query(func.count(ActionPlan.id)).filter( + ActionPlan.verify_result.isnot(None) + ).scalar() or 0 + passed = db.query(func.count(ActionPlan.id)).filter( + ActionPlan.verify_result == "pass" + ).scalar() or 0 + failed = db.query(func.count(ActionPlan.id)).filter( + ActionPlan.verify_result == "fail" + ).scalar() or 0 + with_rule = db.query(func.count(ActionPlan.id)).filter( + ActionPlan.auto_verify_rule.isnot(None) + ).scalar() or 0 + + return { + "total_plans": total, + "verified": verified, + "passed": passed, + "failed": failed, + "with_auto_rule": with_rule, + "pass_rate": round(passed / verified * 100, 1) if verified > 0 else 0, + } + + +# ═══════════════ 端点 ═══════════════ + +@router.get("/iron-law") +def get_iron_law_kpis( + bot: dict = Depends(verify_bot_key), + db: Session = Depends(get_db), + since_days: int = 30, +): + """ + 铁律执行效果KPI看板 + + 返回各Bot的验证统计、通过率、违规数,以及ActionPlan的验证覆盖率。 + 数据来源: + - Hermes 各Profile state.db (消息含 verify/验证/铁律) + - CMA action_plans 表 (verify_result, auto_verify_rule) + """ + # ── 1. 采集各Profile state.db ── + profile_dbs = _list_profile_dbs() + per_bot = {} + total_verify = 0 + total_pass = 0 + total_fail = 0 + total_violations = 0 + + for profile_name, db_path in profile_dbs: + stats = _query_state_db(db_path, since_days=since_days) + per_bot[profile_name] = { + "verification_count": stats["total_verify_msgs"], + "pass_count": stats["pass_msgs"], + "fail_count": stats["fail_msgs"], + "violation_count": stats["violation_msgs"], + "pass_rate": round( + stats["pass_msgs"] / stats["total_verify_msgs"] * 100, 1 + ) if stats["total_verify_msgs"] > 0 else 0, + } + total_verify += stats["total_verify_msgs"] + total_pass += stats["pass_msgs"] + total_fail += stats["fail_msgs"] + total_violations += stats["violation_msgs"] + + # ── 2. 采集ActionPlan验证数据 ── + plan_stats = _query_action_plan_verify(db) + + return { + "bot": bot, + "timestamp": datetime.now().isoformat(), + "period": f"past_{since_days}d", + "summary": { + "total_verifications": total_verify, + "pass_rate": round(total_pass / total_verify * 100, 1) if total_verify > 0 else 0, + "total_violations": total_violations, + "plan_verify_count": plan_stats["verified"], + "plan_pass_rate": plan_stats["pass_rate"], + "plans_with_auto_rule": plan_stats["with_auto_rule"], + "total_action_plans": plan_stats["total_plans"], + }, + "per_bot": per_bot, + "action_plan_verification": { + "total_plans": plan_stats["total_plans"], + "verified": plan_stats["verified"], + "passed": plan_stats["passed"], + "failed": plan_stats["failed"], + "pass_rate": plan_stats["pass_rate"], + "with_auto_rule": plan_stats["with_auto_rule"], + "coverage": round( + plan_stats["verified"] / plan_stats["total_plans"] * 100, 1 + ) if plan_stats["total_plans"] > 0 else 0, + }, + "data_sources": { + "state_dbs": len(profile_dbs), + "profiles_queried": [p[0] for p in profile_dbs], + "action_plans_table": True, + }, + } + + +@router.get("/iron-law/bots") +def get_bot_iron_law_ranking( + bot: dict = Depends(verify_bot_key), + db: Session = Depends(get_db), + since_days: int = 30, +): + """按Bot排名:验证通过率从高到低""" + data = get_iron_law_kpis(bot=bot, db=db, since_days=since_days) + + ranking = sorted( + data["per_bot"].items(), + key=lambda x: x[1]["pass_rate"], + reverse=True, + ) + + ranked = [] + for rank, (name, stats) in enumerate(ranking, 1): + ranked.append({ + "rank": rank, + "bot_name": name, + "pass_rate": stats["pass_rate"], + "verification_count": stats["verification_count"], + "violations": stats["violation_count"], + "failures": stats["fail_count"], + }) + + return { + "bot": bot, + "timestamp": datetime.now().isoformat(), + "period": f"past_{since_days}d", + "ranking": ranked, + "total_bots": len(ranked), + } diff --git a/backend/app/main.py b/backend/app/main.py index 6c5b4d68..575ed95a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,7 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from dotenv import load_dotenv from app.database import init_db -from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis +from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, bot_iron_law from app.utils.cache import clear_all as clear_cache, delete as delete_cache from scripts.erp_sync import run_sync as run_erp_sync from app.auth_middleware import require_auth @@ -71,6 +71,7 @@ app.include_router(okr_templates.router) app.include_router(subjects.router) app.include_router(driver_budget.router) app.include_router(bot_kpis.router) +app.include_router(bot_iron_law.router) @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): diff --git a/frontend/src/components/map-canvas/MapCanvasDialogs.vue b/frontend/src/components/map-canvas/MapCanvasDialogs.vue index 1993a238..89c3da66 100644 --- a/frontend/src/components/map-canvas/MapCanvasDialogs.vue +++ b/frontend/src/components/map-canvas/MapCanvasDialogs.vue @@ -67,11 +67,11 @@