Files
cma-management/backend/scripts/ai_brief.py
T
Hermes CI Fix 3dddd36866 init: 管理会计OS初始代码
包含前后端完整代码:
- 前端:Vue3+Vite+ElementPlus
- 后端:FastAPI+SQLAlchemy
- 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动
- 当前版本:v1.0.0
2026-05-28 17:32:22 +08:00

202 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
定时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))