""" 定时AI经营简报 — 管理会计OS 每天凌晨自动生成经营分析报告,推送至企微 在 daily_sync.py 之后运行 """ import sys import os import json import logging from datetime import datetime sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dotenv import load_dotenv load_dotenv('/root/cma-management/backend/.env') from app.database import get_session_local from app.models import KPIDefinition, KPIValue, KPIAlert, ActionPlan import httpx logger = logging.getLogger("cma.ai_brief") async def call_deepseek(prompt: str, system_prompt: str = None) -> str: """调用DeepSeek API生成分析内容""" api_key = os.getenv("DEEPSEEK_API_KEY", "") api_url = "https://api.deepseek.com/v1/chat/completions" if not system_prompt: system_prompt = "你是一名CMA管理会计师,擅长用数据驱动的方式分析企业经营状况,给出专业的财务分析和管理建议。回答要简洁、专业、有数据支撑。" if not api_key: logger.warning("DEEPSEEK_API_KEY 未配置,跳过AI调用") return "(AI简报暂不可用:API Key未配置)" async with httpx.AsyncClient(timeout=60) as client: resp = await client.post( api_url, headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json={ "model": "deepseek-chat", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} ], "stream": False, "temperature": 0.3, } ) data = resp.json() if resp.status_code != 200: logger.error(f"DeepSeek API错误: {resp.status_code} {data}") return "" return data.get("choices", [{}])[0].get("message", {}).get("content", "") def generate_brief(db_session) -> dict: """生成经营简报""" period = datetime.now().strftime("%Y-%m") kpis = db_session.query(KPIDefinition).filter(KPIDefinition.status == "active").all() kpi_lines = [] for k in kpis: latest = db_session.query(KPIValue).filter( KPIValue.kpi_id == k.id, ).order_by(KPIValue.period.desc()).first() prev_month = f"{int(period[:4])}-{int(period[5:7])-1:02d}" if int(period[5:7]) > 1 else f"{int(period[:4])-1}-12" prev = db_session.query(KPIValue).filter( KPIValue.kpi_id == k.id, KPIValue.period == prev_month ).first() alert = db_session.query(KPIAlert).filter( KPIAlert.kpi_id == k.id, KPIAlert.status == "pending" ).first() if latest and latest.actual_value is not None: line = f"- {k.kpi_name}({k.kpi_code}): {latest.actual_value}{k.unit or ''}" if k.target_value: line += f" | 目标: {k.target_value}" if prev and prev.actual_value: diff = latest.actual_value - prev.actual_value direction = "↑" if diff > 0 else "↓" line += f" | 环比: {direction}{abs(diff):.1f}" if alert: line += f" | ⚠️ {alert.alert_level}预警" kpi_lines.append(line) plans = db_session.query(ActionPlan).order_by(ActionPlan.created_at.desc()).all() plan_lines = [] for p in plans: plan_lines.append(f"- {p.title} | 负责人: {p.assignee} | 状态: {p.status} | 进度: {p.progress}%") kpi_text = "\n".join(kpi_lines) if kpi_lines else "暂无KPI数据" plan_text = "\n".join(plan_lines) if plan_lines else "暂无行动计划" red_count = db_session.query(KPIAlert).filter( KPIAlert.status == "pending", KPIAlert.alert_level == "red" ).count() yellow_count = db_session.query(KPIAlert).filter( KPIAlert.status == "pending", KPIAlert.alert_level == "yellow" ).count() today_str = datetime.now().strftime('%Y-%m-%d') prompt = f"""请为管理层生成一份今日经营简报(日期:{today_str})。 ## 本月KPI数据 {kpi_text} ## 待处理预警 - 红色(紧急): {red_count}条 - 黄色(预警): {yellow_count}条 ## 正在执行的改善行动 {plan_text} 请按以下结构生成简报(不超过800字): 1. 📊 **经营概览**:一句话总结本月经营状况 2. 🔍 **关键发现**:最重要的3个发现(数据驱动) 3. ⚠️ **预警聚焦**:最需要关注的预警及其影响 4. ✅ **行动进展**:改善计划执行情况 5. 💡 **今日建议**:今天最应该做的1-2件事""" try: import asyncio analysis = asyncio.run(call_deepseek(prompt)) except Exception as e: logger.error(f"AI简报生成异常: {e}") analysis = f"简报生成异常: {str(e)}" return { "period": period, "brief": analysis, "kpi_count": len(kpi_lines), "red_alerts": red_count, "yellow_alerts": yellow_count, "plan_count": len(plan_lines), } def push_brief(brief: dict): """将简报推送到企微""" from app.utils.notifier import send_wecom_app db = get_session_local()() try: from app.models import NotificationChannel channels = db.query(NotificationChannel).filter( NotificationChannel.enabled == True, NotificationChannel.channel_type == "wecom_app", ).all() pushed = 0 for ch in channels: config = ch.config or {} result = send_wecom_app( corp_id=config.get("corp_id", ""), corp_secret=config.get("corp_secret", ""), agent_id=config.get("agent_id", ""), touser=config.get("touser", "@all"), title=f"📋 {brief['period']} 经营简报", content=brief["brief"], alert_level="green", ) if result.get("success"): pushed += 1 logger.info(f"简报推送成功: {ch.name}") else: logger.warning(f"简报推送失败: {result.get('message')}") return pushed finally: db.close() def run_brief(): """主入口:生成简报并推送""" logger.info("开始生成AI经营简报...") db = get_session_local()() try: brief = generate_brief(db) logger.info(f"简报生成完成: {brief['period']}, KPI数={brief['kpi_count']}, 预警={brief['red_alerts']}红/{brief['yellow_alerts']}黄") if brief["brief"] and len(brief["brief"]) > 50: pushed = push_brief(brief) logger.info(f"推送完成: {pushed} 个渠道") else: logger.warning("简报内容不足50字,跳过推送") return brief finally: db.close() if __name__ == "__main__": logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) result = run_brief() print(json.dumps(result, ensure_ascii=False, indent=2))