feat: bot-bridge数据通道 — POST /kpi-result回填KPI+触发预警+MPM自动回填脚本

- 新增 POST /api/cma/bot-bridge/kpi-result: (entity_id,kpi_code)查KPI,
  写入KPIValue(source_type=bot, data_status=verified), 复用run_alert_check
  触发预警(自动联动行动计划), 返回new_alerts; KPI不存在/缺字段返回错误
- 修复verify引擎_get_kpi_current_value排序: 按calculated_at取最新值,
  bot回填值可被auto-verify读到 (修复2026H1字符串排序遮蔽月值问题)
- 新增scripts/bot_bridge_push.py: 财务Bot MPM结果自动回填(MPM字段→KPI
  映射), 支持--mpm/--mpm-file/单KPI直推/--ping
This commit is contained in:
Hermes CI Fix
2026-08-10 23:56:14 +08:00
parent f9e20bc9bc
commit 32fe3e4d50
2 changed files with 256 additions and 1 deletions
+138
View File
@@ -0,0 +1,138 @@
"""
bot-bridge 推送脚本 — 财务Bot分析结果自动回填CMA KPI值 + 触发预警
数据通道: 财务Bot MPM结果 → POST /api/cma/bot-bridge/kpi-result → KPIValue写入
→ run_alert_check 触发预警 → 联动行动计划
用法:
# 1) 推送MPM分析结果(自动映射MPM字段→KPI编码,逐项调kpi-result
python3 bot_bridge_push.py --entity-id 1 --period 2026-08 \
--mpm '{"gross_margin_adjusted": 22.5, "channel_rebate_rate": 78.3}'
# 2) 从JSON文件读取MPM结果
python3 bot_bridge_push.py --entity-id 1 --period 2026-08 --mpm-file mpm_result.json
# 3) 单KPI直接回填
python3 bot_bridge_push.py --entity-id 1 --period 2026-08 \
--kpi-code C_REBATE_RATE --value 78.3 --source finance-bot
# 4) 测试连通性
python3 bot_bridge_push.py --ping
环境变量:
CMA_API=http://127.0.0.1:8010/api/cma/bot-bridge (默认)
CMA_BRIDGE_TOKEN=cma-bot-bridge-2026 (默认, 与migrate_bot_bridge_v2.sql一致)
"""
import argparse
import json
import os
import sys
from datetime import datetime
from typing import Optional
import httpx
CMA_API = os.getenv("CMA_API", "http://127.0.0.1:8010/api/cma/bot-bridge")
BRIDGE_TOKEN = os.getenv("CMA_BRIDGE_TOKEN", "cma-bot-bridge-2026")
# MPM字段 → KPI编码映射(与 bot_bridge_v2.MPM_TO_KPI_MAP 保持一致)
MPM_TO_KPI_MAP = {
"gross_margin_adjusted": "F_GROSS_MARGIN",
"net_profit_adjusted": "F_NET_PROFIT",
"channel_rebate_rate": "C_REBATE_RATE",
"mgmt_expense_ratio": "F_COST_RATIO",
}
_HEADERS = {"X-BRIDGE-TOKEN": BRIDGE_TOKEN, "Content-Type": "application/json"}
def push_kpi_result(entity_id: int, period: str, kpi_code: str, value,
source: str = "finance-bot", remark: Optional[str] = None) -> dict:
"""调用 POST /api/cma/bot-bridge/kpi-result"""
payload = {
"entity_id": entity_id,
"kpi_code": kpi_code,
"period": period,
"value": value,
"source": source,
}
if remark:
payload["remark"] = remark
resp = httpx.post(f"{CMA_API}/kpi-result", json=payload,
headers=_HEADERS, timeout=30)
if resp.status_code >= 400:
try:
detail = resp.json().get("detail", resp.text)
except Exception:
detail = resp.text
return {"kpi_code": kpi_code, "success": False, "error": detail}
return {"kpi_code": kpi_code, "success": True, "data": resp.json()}
def push_mpm_results(entity_id: int, period: str, results: dict,
source: str = "finance-bot") -> dict:
"""MPM结果自动回填:映射字段→KPI编码,逐项推送"""
summary = {"kpi_pushed": 0, "kpi_failed": 0, "alerts": [], "items": []}
for field, kpi_code in MPM_TO_KPI_MAP.items():
if not kpi_code or results.get(field) is None:
continue
result = push_kpi_result(
entity_id, period, kpi_code, results[field], source,
remark=f"MPM自动回填[{field}] {datetime.now().strftime('%Y-%m-%d %H:%M')}",
)
summary["items"].append(result)
if result["success"]:
summary["kpi_pushed"] += 1
summary["alerts"].extend(result["data"].get("new_alerts", []))
else:
summary["kpi_failed"] += 1
return summary
def main():
parser = argparse.ArgumentParser(description="财务Bot分析结果回填CMA")
parser.add_argument("--entity-id", type=int, default=1, help="企业实体ID")
parser.add_argument("--period", default=datetime.now().strftime("%Y-%m"), help="期间 YYYY-MM")
parser.add_argument("--mpm", type=json.loads, help="MPM结果JSON (内联)")
parser.add_argument("--mpm-file", help="MPM结果JSON文件路径")
parser.add_argument("--kpi-code", help="单KPI回填: KPI编码")
parser.add_argument("--value", type=float, help="单KPI回填: 数值")
parser.add_argument("--source", default="finance-bot", help="来源Bot标识")
parser.add_argument("--ping", action="store_true", help="连通性测试")
args = parser.parse_args()
if args.ping:
try:
resp = httpx.get(f"{CMA_API}/../bot/ping", headers=_HEADERS, timeout=10)
print(f"✅ bot-bridge API连通 (HTTP {resp.status_code})")
return 0
except Exception as e:
print(f"❌ 连接失败: {e}")
return 1
if args.mpm_file:
with open(args.mpm_file, encoding="utf-8") as f:
mpm = json.load(f)
else:
mpm = args.mpm
if mpm:
summary = push_mpm_results(args.entity_id, args.period, mpm, args.source)
print(json.dumps(summary, ensure_ascii=False, indent=2))
return 0 if summary["kpi_failed"] == 0 else 1
if args.kpi_code:
if args.value is None:
print("❌ 单KPI回填需要 --value")
return 1
result = push_kpi_result(args.entity_id, args.period, args.kpi_code,
args.value, args.source)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result["success"] else 1
parser.print_help()
return 1
if __name__ == "__main__":
sys.exit(main())