172 lines
5.9 KiB
Python
172 lines
5.9 KiB
Python
"""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"}
|