Files
aiagent/backend/app/api/feishu_bind.py

244 lines
8.3 KiB
Python
Raw Normal View History

"""飞书绑定 API — 绑定/解绑/事件回调"""
from __future__ import annotations
import json
import logging
from typing import Any, Dict
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.api.auth import get_current_user
from app.core.database import get_db, SessionLocal
from app.models.user import User
from app.models.agent import Agent
from app.services.feishu_ws_handler import get_pending_open_ids, clear_pending_open_ids
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/feishu", tags=["feishu"])
# ─── 飞书事件回调HTTP 模式备用,主用长连接)────────────────────
@router.post("/event", include_in_schema=False)
async def feishu_event_callback(request: Request):
"""飞书事件回调 — 处理 URL 验证HTTP 回调模式备用)。
如果使用长连接订阅方式事件通过 WebSocket 推送此端点不需配置
"""
from app.services.feishu_app_service import get_verification_token
from app.services.feishu_ws_handler import _pending_open_ids
try:
body = await request.json()
except Exception:
return {"error": "invalid json"}
# ── URL 验证 ──
if body.get("type") == "url_verify":
challenge = body.get("challenge")
token = body.get("token", "")
logger.info("飞书事件 URL 验证: challenge=%s", challenge)
if token != get_verification_token():
logger.warning("飞书事件 token 不匹配")
return {"challenge": challenge}
return {"challenge": challenge}
# ── 事件回调 ──
if body.get("type") == "event_callback":
token = body.get("token", "")
if token != get_verification_token():
logger.warning("飞书事件 token 不匹配")
return {"error": "invalid token"}
event = body.get("event", {})
event_type = event.get("type")
if event_type == "im.message.receive_v1":
sender = event.get("sender", {})
sender_id = sender.get("sender_id", {})
open_id = sender_id.get("open_id", "")
message = event.get("message", {})
chat_type = message.get("chat_type", "p2p") # p2p = 私聊
logger.info("飞书 HTTP 回调收到消息: open_id=%s chat_type=%s", open_id[:20], chat_type)
if open_id and chat_type == "p2p":
_pending_open_ids.append(open_id)
if len(_pending_open_ids) > 5:
_pending_open_ids.pop(0)
return {"code": 0, "msg": "ok"}
return {"error": "unknown event type"}
class BindFeishuRequest(BaseModel):
open_id: str
@router.post("/bind")
async def bind_feishu(
data: BindFeishuRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""绑定飞书用户 open_id 到当前账号。"""
if not data.open_id or not data.open_id.strip():
raise HTTPException(status_code=400, detail="open_id 不能为空")
current_user.feishu_open_id = data.open_id.strip()
db.commit()
logger.info("飞书绑定成功: user=%s open_id=%s", current_user.id, data.open_id)
return {"message": "飞书账号绑定成功"}
@router.post("/unbind")
async def unbind_feishu(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""解绑飞书账号。"""
current_user.feishu_open_id = None
db.commit()
logger.info("飞书解绑成功: user=%s", current_user.id)
return {"message": "飞书账号解绑成功"}
@router.post("/lookup")
async def lookup_and_bind(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""通过当前用户的邮箱自动查找并绑定飞书账号。
需要飞书应用已开通 contact:user.employee_id:readonly 权限
系统会使用用户的注册邮箱在飞书中搜索匹配的 open_id 并自动绑定
"""
if not current_user.email:
raise HTTPException(status_code=400, detail="当前用户没有邮箱信息")
from app.services.feishu_app_service import lookup_user_by_email
open_id = lookup_user_by_email(current_user.email)
if not open_id:
raise HTTPException(
status_code=404,
detail=f"在飞书中未找到邮箱 {current_user.email} 对应的用户。"
f"请确认1) 该邮箱已在飞书通讯录中 2) 应用已拥有 contact:user.employee_id:readonly 权限",
)
current_user.feishu_open_id = open_id
db.commit()
logger.info("飞书自动绑定成功: user=%s email=%s open_id=%s", current_user.id, current_user.email, open_id)
# 发送测试消息
from app.services.feishu_app_service import send_message_to_user
send_message_to_user(
open_id=open_id,
title="飞书通知绑定成功 🎉",
content=f"你好 {current_user.username},你的平台账号已成功绑定飞书。\n\n"
f"从此开始Agent 定时任务的执行结果将通过飞书实时推送给你。",
status="success",
)
return {
"message": "飞书账号绑定成功",
"open_id": open_id,
"test_message_sent": True,
}
@router.get("/pending")
async def get_pending_open_ids_api(
current_user: User = Depends(get_current_user),
):
"""获取通过飞书消息事件捕获的 open_id 列表。
在飞书里给苹果应用发一条任意消息后
调用此接口查看是否有待绑定的 open_id
"""
return {"pending_ids": get_pending_open_ids()}
@router.post("/bind-pending")
async def bind_pending(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""绑定最近一条从飞书事件捕获的 open_id。"""
ids = get_pending_open_ids()
if not ids:
raise HTTPException(status_code=404, detail="没有待绑定的 open_id请在飞书里给应用发一条消息")
open_id = ids[-1]
current_user.feishu_open_id = open_id
db.commit()
clear_pending_open_ids()
logger.info("飞书事件绑定成功: user=%s open_id=%s", current_user.id, open_id)
# 发送测试消息
from app.services.feishu_app_service import send_message_to_user
send_message_to_user(
open_id=open_id,
title="飞书通知绑定成功",
content=f"你好 {current_user.username},你的平台账号已成功绑定飞书。\n定时任务执行结果将通过飞书实时推送给你。",
status="success",
)
return {"message": "飞书账号绑定成功", "open_id": open_id}
@router.get("/default-agent")
async def get_default_agent(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""获取飞书对话默认 Agent。"""
agent_id = current_user.feishu_default_agent_id
agent_name = None
if agent_id:
agent = db.query(Agent).filter(Agent.id == agent_id).first()
agent_name = agent.name if agent else None
return {
"agent_id": agent_id,
"agent_name": agent_name,
}
class SetDefaultAgentRequest(BaseModel):
agent_id: str
@router.post("/default-agent")
async def set_default_agent(
data: SetDefaultAgentRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""设置飞书对话默认 Agent。"""
agent = db.query(Agent).filter(Agent.id == data.agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
if agent.user_id and agent.user_id != current_user.id and current_user.role != "admin":
raise HTTPException(status_code=403, detail="无权使用该 Agent")
current_user.feishu_default_agent_id = data.agent_id
db.commit()
logger.info("飞书默认 Agent 设置成功: user=%s agent=%s", current_user.id, data.agent_id)
return {"message": f"默认 Agent 已设置为「{agent.name}", "agent_id": data.agent_id}
@router.get("/status")
async def feishu_status(
current_user: User = Depends(get_current_user),
):
"""查询当前用户飞书绑定状态。"""
return {
"bound": bool(current_user.feishu_open_id),
"open_id": current_user.feishu_open_id,
}