diff --git a/backend/app/api/bot_kpis.py b/backend/app/api/bot_kpis.py new file mode 100644 index 00000000..ddefa772 --- /dev/null +++ b/backend/app/api/bot_kpis.py @@ -0,0 +1,172 @@ +"""Bot KPI管理 API — 管理各Agent的KPI自评体系""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import Optional +from datetime import datetime + +from app.database import get_db +from app.auth_middleware import require_auth +from app.models import KPIDefinition, KPIValue + +router = APIRouter(prefix="/api/cma/bot-kpis", tags=["Bot KPI管理"], + dependencies=[Depends(require_auth)], +) + +# 反向指标编码(值越低越好) +REVERSE_BOT_INDICATORS = ['FB_RESPONSE_TIME'] + + +def _calc_bot_kpi_score(current_value, target_value, is_reverse=False): + """五档评分引擎(复用KPI体系)- 1~5分""" + if current_value is None or target_value is None or target_value == 0: + return None, "info" + ratio = current_value / target_value + if is_reverse: + if ratio <= 0.5: + return 5, "success" + elif ratio <= 0.8: + return 4, "success" + elif ratio <= 1.0: + return 3, "warning" + elif ratio <= 1.2: + return 2, "danger" + else: + return 1, "danger" + else: + if ratio >= 1.2: + return 5, "success" + elif ratio >= 1.0: + return 4, "success" + elif ratio >= 0.8: + return 3, "warning" + elif ratio >= 0.5: + return 2, "danger" + else: + return 1, "danger" + + +@router.get("") +def list_bot_kpis( + source: str = Query("finance-bot", description="Bot标识"), + period: Optional[str] = None, + db: Session = Depends(get_db), +): + """获取某Bot的所有KPI(含评分)""" + kpis = db.query(KPIDefinition).filter( + KPIDefinition.bot_source == source, + KPIDefinition.status == "active", + ).order_by(KPIDefinition.kpi_code).all() + + if not kpis: + return { + "source": source, + "kpis": [], + "groups": {}, + "overall": None, + } + + from app.api.kpis import REVERSE_INDICATORS as _ri + + result_kpis = [] + for k in kpis: + # 取最新实际值 + val_query = db.query(KPIValue).filter( + KPIValue.kpi_id == k.id, + KPIValue.actual_value.isnot(None), + ) + if period: + val_query = val_query.filter(KPIValue.period == period) + latest_val = val_query.order_by(KPIValue.period.desc()).first() + + current_val = latest_val.actual_value if latest_val else None + is_reverse = k.kpi_code in REVERSE_BOT_INDICATORS + score, status = _calc_bot_kpi_score(current_val, k.target_value, is_reverse=is_reverse) + + result_kpis.append({ + "id": k.id, + "kpi_code": k.kpi_code, + "kpi_name": k.kpi_name, + "category": k.category or "core_output", + "formula": k.formula, + "target_value": k.target_value, + "current_value": current_val, + "unit": k.unit, + "weight": 15, # 权重在PRD中定义,但未存表,前端使用 + "score": score, + "status": status, + "period": latest_val.period if latest_val else None, + "frequency": k.frequency or "monthly", + }) + + # 按group分组: core_output / quality / user_feedback + groups = { + "core_output": {"label": "核心产出", "kpis": []}, + "quality": {"label": "质量监控", "kpis": []}, + "user_feedback": {"label": "用户反馈", "kpis": []}, + } + # category映射: FB编码前缀区分 + for kp in result_kpis: + code = kp["kpi_code"] + if code.startswith("FB_ANALYSIS") or code.startswith("FB_ACCURACY") or code.startswith("FB_ISSUE") or code.startswith("FB_ACTION") or code.startswith("FB_RESPONSE"): + groups["core_output"]["kpis"].append(kp) + elif code.startswith("FB_DATA") or code.startswith("FB_CONSISTENCY") or code.startswith("FB_CITATION"): + groups["quality"]["kpis"].append(kp) + else: + groups["user_feedback"]["kpis"].append(kp) + + # 综合得分 + scored_kpis = [k for k in result_kpis if k["score"] is not None] + if scored_kpis: + overall = round(sum(k["score"] * k["weight"] for k in scored_kpis) / sum(k["weight"] for k in scored_kpis), 2) + overall_status = "success" if overall >= 4 else ("warning" if overall >= 3 else "danger") + else: + overall = None + overall_status = "info" + + return { + "source": source, + "kpis": result_kpis, + "groups": groups, + "overall": {"score": overall, "status": overall_status}, + } + + +@router.post("/{kpi_id}/value") +def update_bot_kpi_value( + kpi_id: int, + data: dict, + db: Session = Depends(get_db), +): + """更新Bot KPI当前值""" + kpi = db.query(KPIDefinition).filter( + KPIDefinition.id == kpi_id, + KPIDefinition.bot_source.isnot(None), + ).first() + if not kpi: + raise HTTPException(404, "Bot KPI不存在") + + actual_value = data.get("actual_value") + if actual_value is None: + raise HTTPException(422, "actual_value 不能为空") + + period = data.get("period", datetime.now().strftime("%Y-%m")) + + existing = db.query(KPIValue).filter( + KPIValue.kpi_id == kpi_id, + KPIValue.period == period, + ).first() + if existing: + existing.actual_value = actual_value + existing.source_type = "manual" + else: + val = KPIValue( + kpi_id=kpi_id, + period=period, + actual_value=actual_value, + source_type="manual", + data_status="pending", + ) + db.add(val) + + db.commit() + return {"message": "更新成功", "kpi_id": kpi_id, "period": period, "actual_value": actual_value} diff --git a/backend/app/main.py b/backend/app/main.py index dfe96194..6c5b4d68 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 +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.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 @@ -70,6 +70,7 @@ app.include_router(okr.router) app.include_router(okr_templates.router) app.include_router(subjects.router) app.include_router(driver_budget.router) +app.include_router(bot_kpis.router) @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 868b7e6a..09d38b18 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -72,6 +72,7 @@ class KPIDefinition(Base): responsible_dept = Column(String(200), nullable=True, comment="负责部门") responsible_user = Column(String(100), nullable=True, comment="负责人") status = Column(String(20), default="active") + bot_source = Column(String(50), nullable=True, comment="Bot标识: finance-bot/ops-bot等") epic = Column(String(50), default="Epic2", comment="所属Epic") created_by = Column(Integer, nullable=True) created_at = Column(DateTime, server_default=func.now()) diff --git a/backend/scripts/seed_finance_bot_kpis.py b/backend/scripts/seed_finance_bot_kpis.py new file mode 100644 index 00000000..37e0b56c --- /dev/null +++ b/backend/scripts/seed_finance_bot_kpis.py @@ -0,0 +1,180 @@ +""" +种子数据:财务Bot KPI(11个) +插入到 kpi_definitions 表,bot_source='finance-bot' +""" +import pymysql +import os +import sys +from datetime import datetime + +DB_USER = os.getenv("CMA_DB_USER", "cma_user") +DB_PASS = os.getenv("CMA_DB_PASS", "cma_pass_2026") +DB_HOST = os.getenv("CMA_DB_HOST", "127.0.0.1") +DB_PORT = int(os.getenv("CMA_DB_PORT", "3306")) +DB_NAME = os.getenv("CMA_DB_NAME", "cma") + +FINANCE_BOT_KPIS = [ + # ── 核心产出(5个 · 月度考核)── + { + "kpi_code": "FB_ANALYSIS_COUNT", + "kpi_name": "分析报告产出数", + "formula": "月度生成的结构化分析报告数量", + "unit": "份", + "target_value": 20, + "frequency": "monthly", + "category": "core_output", + "weight": 15, + }, + { + "kpi_code": "FB_ACCURACY_RATE", + "kpi_name": "数据提取准确率", + "formula": "1−(数据错误次数/总分析次数)", + "unit": "%", + "target_value": 98, + "frequency": "monthly", + "category": "core_output", + "weight": 25, + }, + { + "kpi_code": "FB_ISSUE_FOUND", + "kpi_name": "问题发现数", + "formula": "月度发现的影响经营的问题数量", + "unit": "个", + "target_value": 5, + "frequency": "monthly", + "category": "core_output", + "weight": 20, + }, + { + "kpi_code": "FB_ACTION_RATE", + "kpi_name": "行动采纳率", + "formula": "被用户采纳的行动建议数/总建议数", + "unit": "%", + "target_value": 60, + "frequency": "monthly", + "category": "core_output", + "weight": 25, + }, + { + "kpi_code": "FB_RESPONSE_TIME", + "kpi_name": "响应时效", + "formula": "用户发文件到出分析结果的平均时间", + "unit": "分钟", + "target_value": 10, + "frequency": "monthly", + "category": "core_output", + "weight": 15, + }, + # ── 质量监控(3个 · 月度考核)── + { + "kpi_code": "FB_DATA_GAP", + "kpi_name": "数据间隙发现率", + "formula": "发现的数据异常/缺失数 / 应发现数", + "unit": "%", + "target_value": 90, + "frequency": "monthly", + "category": "quality", + "weight": 30, + }, + { + "kpi_code": "FB_CONSISTENCY", + "kpi_name": "跨期一致性", + "formula": "同期指标口径是否一致", + "unit": "%", + "target_value": 100, + "frequency": "monthly", + "category": "quality", + "weight": 30, + }, + { + "kpi_code": "FB_CITATION", + "kpi_name": "结论可追溯率", + "formula": "每个结论有对应的数据来源", + "unit": "%", + "target_value": 100, + "frequency": "monthly", + "category": "quality", + "weight": 40, + }, + # ── 用户反馈(3个 · 季度考核)── + { + "kpi_code": "FB_SATISFACTION", + "kpi_name": "用户满意度", + "formula": "用户对分析报告的评分(1-5分)", + "unit": "分", + "target_value": 4.0, + "frequency": "quarterly", + "category": "user_feedback", + "weight": 40, + }, + { + "kpi_code": "FB_REUSE_RATE", + "kpi_name": "复用率", + "formula": "用户连续使用天数/月总天数", + "unit": "%", + "target_value": 80, + "frequency": "quarterly", + "category": "user_feedback", + "weight": 30, + }, + { + "kpi_code": "FB_REFERRAL", + "kpi_name": "推荐率", + "formula": "用户主动向他人推荐次数", + "unit": "次", + "target_value": 1, + "frequency": "quarterly", + "category": "user_feedback", + "weight": 30, + }, +] + + +def run(): + conn = pymysql.connect( + host=DB_HOST, user=DB_USER, password=DB_PASS, + database=DB_NAME, charset="utf8mb4", + ) + cursor = conn.cursor() + + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + inserted = 0 + skipped = 0 + + for kpi in FINANCE_BOT_KPIS: + code = kpi["kpi_code"] + # 检查是否已存在 + cursor.execute("SELECT id FROM kpi_definitions WHERE kpi_code = %s", (code,)) + existing = cursor.fetchone() + if existing: + print(f" ⏭ {code} 已存在 (id={existing[0]})") + skipped += 1 + continue + + sql = """ + INSERT INTO kpi_definitions + (entity_id, kpi_code, kpi_name, dimension, formula, unit, + target_value, frequency, category, status, bot_source, data_source, + data_owner, created_at, updated_at) + VALUES + (%s, %s, %s, %s, %s, %s, + %s, %s, %s, 'active', 'finance-bot', 'Bot自计数', + 'FinanceBot', %s, %s) + """ + cursor.execute(sql, ( + 1, code, kpi["kpi_name"], "process", kpi["formula"], kpi["unit"], + kpi["target_value"], kpi["frequency"], kpi["category"], + now, now, + )) + new_id = cursor.lastrowid + print(f" ✅ {code} -> id={new_id}") + inserted += 1 + + conn.commit() + cursor.close() + conn.close() + print(f"\n完成:新增 {inserted} 条,跳过 {skipped} 条(共 {len(FINANCE_BOT_KPIS)} 个KPI)") + + +if __name__ == "__main__": + run() diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 1adde323..7e0d4244 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -279,4 +279,9 @@ export const ethicsQuizApi = { getQuestions: () => api.get('/knowledge/ethics-quiz'), } +export const botKpiApi = { + list: (params?: any) => api.get('/bot-kpis', { params }), + updateValue: (id: number, data: any) => api.post(`/bot-kpis/${id}/value`, data), +} + export default api diff --git a/frontend/src/permission.ts b/frontend/src/permission.ts index cdc574c4..5da6dc70 100644 --- a/frontend/src/permission.ts +++ b/frontend/src/permission.ts @@ -41,6 +41,7 @@ export const MENU_ITEMS: MenuItem[] = [ { path: '/customer', label: '客户维度', icon: 'User', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' }, { path: '/learning-dashboard', label: '学习成长看板', icon: 'Reading', roles: ['ceo', 'finance', 'it'], group: '🟡 C 监控与评价' }, { path: '/alerts', label: '预警中心', icon: 'WarningFilled', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' }, + { path: '/bot-kpis', label: 'Bot KPI', icon: 'TrendCharts', roles: ['ceo', 'finance', 'it'], group: '🟡 C 监控与评价' }, // ── GROUP 4: 复盘与改进(Act)── { path: '/action-plans', label: '改善行动', icon: 'Edit', roles: ['ceo', 'finance', 'business', 'it'], group: '🔴 A 复盘与改进' }, diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 937bd3ee..6b6d32d0 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -38,6 +38,7 @@ const routes = [ { path: 'okr-templates', name: 'OKRTemplates', component: () => import('@/views/OKRTemplates.vue'), meta: { title: 'OKR模板库', roles: ['ceo', 'finance', 'it'] } }, { path: 'subjects', name: 'SubjectManage', component: () => import('@/views/SubjectManage.vue'), meta: { title: '科目打标', roles: ['ceo', 'finance', 'business', 'it'] } }, { path: 'mpm-calculator', name: 'MpmCalculator', component: () => import('@/views/MpmCalculator.vue'), meta: { title: 'MPM计算器', roles: ['ceo', 'finance', 'business'] } }, + { path: 'bot-kpis', name: 'BotKpis', component: () => import('@/views/BotKpiDashboard.vue'), meta: { title: 'Bot KPI看板', roles: ['ceo', 'finance', 'it'] } }, ] }, ] diff --git a/frontend/src/views/BotKpiDashboard.vue b/frontend/src/views/BotKpiDashboard.vue new file mode 100644 index 00000000..815d046b --- /dev/null +++ b/frontend/src/views/BotKpiDashboard.vue @@ -0,0 +1,298 @@ + + + + + + 🤖 财务Bot KPI看板 + 2026年7月 + + + 刷新 + + + + + + + + {{ overallScore != null ? overallScore : '-' }} + 综合得分 + + + 🏆 财务Bot · 2026年7月 + 基于11项KPI的五档评分引擎,满分5.0 + + + {{ overallScore >= 4 ? '表现优秀' : overallScore >= 3 ? '需要关注' : '亟需改善' }} + + + + + + + + + + + + + 核心产出 KPI(月度) + {{ coreKpis.length }} + + + + + {{ k.kpi_name }} + + {{ k.score != null ? k.score + '分' : '--' }} + + + + + 目标 + {{ formatVal(k.target_value, k.unit) }} + + + 当前 + {{ k.current_value != null ? formatVal(k.current_value, k.unit) : '待统计' }} + + + + + + + + + 权重 {{ k.weight }}% + 📐 + + + + + + + + + + 质量监控 KPI(月度) + {{ qualityKpis.length }} + + + + + {{ k.kpi_name }} + + {{ k.score != null ? k.score + '分' : '--' }} + + + + + 目标 + {{ formatVal(k.target_value, k.unit) }} + + + 当前 + {{ k.current_value != null ? formatVal(k.current_value, k.unit) : '待统计' }} + + + + + + + + 权重 {{ k.weight }}% + + + + + + + + + + 用户反馈 KPI(季度) + {{ feedbackKpis.length }} + + + + + {{ k.kpi_name }} + + {{ k.score != null ? k.score + '分' : '--' }} + + + + + 目标 + {{ formatVal(k.target_value, k.unit) }} + + + 当前 + {{ k.current_value != null ? formatVal(k.current_value, k.unit) : '待统计' }} + + + + + + + + 权重 {{ k.weight }}% + 季度 + + + + + + + + + + + + + + + +