"""Webhook 接收 API""" import json import re from datetime import datetime, timezone from fastapi import APIRouter, Depends, Request from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from ..database import get_db from ..models import Signal from ..services.llm import llm_service from ..services.plane import plane_service from ..services.lark import LarkService router = APIRouter(prefix="/webhooks", tags=["webhooks"]) lark_service = LarkService() @router.post("/git") async def handle_git_webhook( payload: dict, db: AsyncSession = Depends(get_db), ): """接收 Git Webhook,审计代码并更新状态""" commits = payload.get("commits", []) if not commits: return {"status": "ignored", "reason": "No commits found"} updated_tasks = [] for commit in commits: message = commit.get("message", "") code_diff = commit.get("patch") or str(commit.get("modified", [])) task_matches = re.findall( r"#([A-Za-z0-9-]+-\d+|[A-Za-z0-9]+-\d+)", message ) for task_id in task_matches: result = await db.execute( select(Signal).order_by(Signal.created_at.desc()).limit(20) ) signals = result.scalars().all() target_task = None target_signal = None for sig in signals: if not sig.tasks_generated: continue try: tasks = json.loads(sig.tasks_generated) for t in tasks: if t.get("plane_key") == task_id or t.get( "plane_id" ) == task_id: target_task = t target_signal = sig break except Exception: continue if target_task: break task_name = ( target_task.get("task_name") if target_task else f"Task {task_id}" ) # 尽量从目标信号推断租户,避免跨租户预算串账。 tenant_id = int(getattr(target_signal, "tenant_id", 1) or 1) try: audit_result = await llm_service.audit_code_change_async( task_name, message, code_diff, db=db, tenant_id=tenant_id, ) except RuntimeError: updated_tasks.append( { "task_id": task_id, "status": "Blocked", "audit": {"passed": False, "reason": "预算超限,AI 审计被阻止"}, } ) continue if audit_result.get("passed"): await plane_service.update_issue_status(task_id, status="Done") if target_signal and target_task: tasks = json.loads(target_signal.tasks_generated) for t in tasks: if t.get("plane_key") == task_id or t.get( "plane_id" ) == task_id: t["status"] = "Done" t["audit_feedback"] = audit_result.get("reason") t["completed_at"] = ( datetime.now(timezone.utc).isoformat() ) target_signal.tasks_generated = json.dumps(tasks) await db.commit() updated_tasks.append( {"task_id": task_id, "status": "Done", "audit": audit_result} ) else: updated_tasks.append( { "task_id": task_id, "status": "Failed", "audit": audit_result, } ) return {"status": "processed", "updated_tasks": updated_tasks} @router.post("/lark") async def handle_lark_webhook( request: Request, db: AsyncSession = Depends(get_db), ): """接收飞书 Bot Webhook""" try: data = await request.json() except Exception: return {"status": "error", "message": "Invalid JSON"} if "challenge" in data: return {"challenge": data["challenge"]} event = data.get("event", {}) if not event: return {"status": "ignored"} if event.get("type") == "message" or event.get("msg_type") == "text": message_content = event.get("text_without_at_bot") or event.get( "content", "" ) if isinstance(message_content, str) and message_content.startswith("{"): try: content_obj = json.loads(message_content) message_content = content_obj.get("text", "") except Exception: pass message_content = message_content.strip() chat_id = event.get("open_chat_id") or event.get("chat_id") if not message_content or not chat_id: return {"status": "ignored", "reason": "No content or chat_id"} try: try: tasks = await llm_service.generate_task_breakdown_async( message_content, db=db, tenant_id=1, # Webhook 来源尚未绑定租户,先按默认租户计费 ) except RuntimeError: return {"status": "blocked", "reason": "budget_exceeded_hard_limit"} if tasks: await plane_service.sync_tasks(tasks) await lark_service.send_card_message( chat_id, message_content, tasks ) except Exception as e: print(f"Error processing Lark message: {e}") return {"status": "processed"}