feat: 编辑目标——回车自动加%+权重合计校验
This commit is contained in:
@@ -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),
|
||||
}
|
||||
+2
-1
@@ -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):
|
||||
|
||||
@@ -67,11 +67,11 @@
|
||||
<div class="mc-kr-row-bottom">
|
||||
<div class="mc-kr-field">
|
||||
<label>目标值</label>
|
||||
<input v-model="kr.target_value" class="mc-input mc-kr-input" placeholder="如 ≥18%" />
|
||||
<input v-model="kr.target_value" class="mc-input mc-kr-input" placeholder="输入数字,回车自动加%" @keydown.enter.prevent="formatTargetValue(ki)" />
|
||||
</div>
|
||||
<div class="mc-kr-field">
|
||||
<label>权重</label>
|
||||
<select v-model="kr.weight" class="mc-input mc-kr-input">
|
||||
<select v-model="kr.weight" class="mc-input mc-kr-input" @change="checkWeightSum">
|
||||
<option value="10">10%</option>
|
||||
<option value="20">20%</option>
|
||||
<option value="25">25%</option>
|
||||
@@ -86,6 +86,7 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="weightSum !== null && weightSum !== 100" class="mc-kr-warning">⚠️ 权重合计 {{ weightSum }}%,应为100%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -496,6 +497,10 @@ function prevStep() {
|
||||
selectedTemplateIdx.value = null
|
||||
}
|
||||
|
||||
const weightSum = computed(() => {
|
||||
return form.value.krs.reduce((s, kr) => s + (parseInt(kr.weight) || 0), 0)
|
||||
})
|
||||
|
||||
function addKr() {
|
||||
form.value.krs.push({ name: "", target_value: "", weight: "33" })
|
||||
}
|
||||
@@ -504,6 +509,17 @@ function removeKr(idx: number) {
|
||||
form.value.krs.splice(idx, 1)
|
||||
}
|
||||
|
||||
function formatTargetValue(idx: number) {
|
||||
const val = form.value.krs[idx]?.target_value
|
||||
if (val && !val.includes('%') && !isNaN(parseFloat(val))) {
|
||||
form.value.krs[idx].target_value = parseFloat(val).toFixed(2) + '%'
|
||||
}
|
||||
}
|
||||
|
||||
function checkWeightSum() {
|
||||
// 触发computed重新计算,warning自动显示
|
||||
}
|
||||
|
||||
function onSave() {
|
||||
if (!form.value.o_name?.trim()) {
|
||||
// 简单提示 — 父组件会检测
|
||||
@@ -772,3 +788,4 @@ function planStatusLabel(s: string): string {
|
||||
.plan-popup-title { font-size: 13px; font-weight: 500; }
|
||||
.plan-popup-meta { display: flex; gap: 12px; margin-top: 4px; font-size: 12px; color: #909399; }
|
||||
</style>
|
||||
.mc-kr-warning { color: #e6a23c; font-size: 12px; margin-top: 4px; }
|
||||
|
||||
Reference in New Issue
Block a user