Files
company-ai-platform/app/modules/feishu/events.py
JiuContinent 92f490b97e ```
feat(core): 添加多API密钥支持和配置字段

添加了api_keys、audit_api_keys、approval_api_keys等字段用于支持多个服务密钥,
新增masked_response_fields用于配置响应掩码字段,以及legacy相关配置项。

feat(core): 增强响应数据掩码功能

扩展mask_configured函数支持域名参数,实现更精确的敏感字段掩码控制,
添加自定义掩码字段配置验证器。

feat(scheduler): 添加遗留系统同步调度任务

集成遗留项目和任务同步到定时调度器中,支持通过配置启用或禁用同步功能,
并可设置不同的执行时间计划。

feat(security): 实现多服务密钥认证机制

重构API密钥验证逻辑,支持单个主密钥和多个配置密钥的混合验证模式,
增加服务密钥启用状态检查和角色映射功能。

feat(task_queue): 扩展现有队列任务处理

为日常简报和周报推送任务添加Celery异步处理支持,新增遗留项目和任务同步任务,
统一任务分发接口。

feat(business): 扩展业务模型字段

为工作任务模型添加外部系统标识和外部ID字段,为风险事件模型增加分配、解决、关闭
等相关字段,并创建风险事件操作记录表。

feat(legacy_mysql): 实现遗留任务同步功能

添加遗留任务查询和同步路由,支持从旧MySQL数据库同步任务数据到内部系统,
包括同步结果统计和运行记录。

refactor(dashboard): 更新仪表板统计数据

增加未分配风险和失败推送运行统计,在概览中显示最新的推送和同步运行记录,
完善数据序列化展示。

fix(feishu): 修复审批事件重复处理

实现审批卡片操作事件的唯一性检查,防止重复审批操作,添加事件审计日志记录。
```
2026-07-08 12:05:09 +08:00

228 lines
8.4 KiB
Python

import json
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.core.constants import ActorValue
from app.modules.approvals.service import ApprovalService
from app.modules.audit.constants import AuditAction, AuditSource
from app.modules.audit.schemas import AuditLogCreate
from app.modules.feishu.commands import FeishuCommandService
from app.modules.feishu.constants import (
FeishuCommandKey,
FeishuEventReceiptKey,
FeishuEventSource,
FeishuPayloadKey,
FeishuResponseKey,
)
from app.modules.feishu.models import FeishuEventReceipt
from app.modules.feishu.service import FeishuService
FEISHU_EVENT_ACTIONS = {
FeishuEventSource.WEBHOOK: AuditAction.FEISHU_WEBHOOK_EVENT,
FeishuEventSource.LONG_CONNECTION: AuditAction.FEISHU_LONG_CONNECTION_EVENT,
}
class FeishuEventService:
"""Handle Feishu message events from webhook or long connection."""
def __init__(self, db: Session):
self.db = db
self.feishu = FeishuService(db)
self.commands = FeishuCommandService(db)
def handle_event(
self,
payload: dict[str, Any],
source: str | FeishuEventSource,
auto_reply: bool = True,
) -> dict[str, Any]:
self.feishu.verify_event(payload)
challenge = payload.get(FeishuPayloadKey.CHALLENGE)
if challenge:
return {FeishuResponseKey.CHALLENGE: challenge}
source_value = _normalize_source(source)
event_identity = _event_identity(payload, source)
if event_identity and not self._register_event(event_identity):
return {
FeishuResponseKey.OK: True,
FeishuResponseKey.HANDLED: False,
FeishuResponseKey.DUPLICATE: True,
}
self.feishu.audit.log(
AuditLogCreate(
actor=ActorValue.FEISHU,
source=AuditSource.FEISHU,
action=FEISHU_EVENT_ACTIONS[source_value],
target_type=source_value,
target_id=(
event_identity.get(FeishuEventReceiptKey.EVENT_KEY)
if event_identity
else None
),
request_payload=payload,
response_payload={FeishuResponseKey.ACCEPTED: True},
)
)
command = self.commands.extract_event_command(payload)
if not command:
return {FeishuResponseKey.OK: True, FeishuResponseKey.HANDLED: False}
result = self.commands.handle_text(
command[FeishuCommandKey.TEXT],
chat_id=command[FeishuCommandKey.CHAT_ID],
actor=command[FeishuCommandKey.ACTOR],
auto_reply=auto_reply,
)
return {
FeishuResponseKey.OK: True,
FeishuResponseKey.HANDLED: True,
FeishuResponseKey.RESULT: result,
}
def handle_approval_card_action(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Handle Feishu interactive-card approval button callbacks."""
self.feishu.verify_event(payload)
value = _approval_action_value(payload)
ticket_id = str(value.get("ticket_id") or "").strip()
decision = str(value.get("decision") or value.get("action") or "").lower()
if not ticket_id or decision not in {"approve", "reject"}:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid Feishu approval action payload",
)
comment = value.get("comment")
actor = _approval_operator(payload)
event_identity = _approval_event_identity(payload, ticket_id, decision, actor)
if not self._register_event(event_identity):
ticket = ApprovalService(self.db).get_by_ticket(ticket_id)
return {
FeishuResponseKey.OK: True,
FeishuResponseKey.HANDLED: True,
FeishuResponseKey.DUPLICATE: True,
FeishuResponseKey.RESULT: {
"ticket_id": ticket.ticket_id,
"status": ticket.status,
"approver": ticket.approver,
},
}
ticket = ApprovalService(self.db).decide(
ticket_id,
actor,
approved=decision == "approve",
comment=str(comment) if comment is not None else None,
)
self.feishu.audit.log(
AuditLogCreate(
actor=actor,
source=AuditSource.FEISHU,
action=AuditAction.FEISHU_WEBHOOK_EVENT,
target_type="approval_card_action",
target_id=ticket_id,
request_payload=payload,
response_payload={"status": ticket.status, "decision": decision},
)
)
return {
FeishuResponseKey.OK: True,
FeishuResponseKey.HANDLED: True,
FeishuResponseKey.RESULT: {
"ticket_id": ticket.ticket_id,
"status": ticket.status,
"approver": ticket.approver,
},
}
def _register_event(self, event_identity: dict[str, str | None]) -> bool:
receipt = FeishuEventReceipt(
event_key=str(event_identity[FeishuEventReceiptKey.EVENT_KEY]),
source=str(event_identity[FeishuEventReceiptKey.SOURCE]),
event_id=event_identity.get(FeishuEventReceiptKey.EVENT_ID),
message_id=event_identity.get(FeishuEventReceiptKey.MESSAGE_ID),
)
self.db.add(receipt)
try:
self.db.flush()
except IntegrityError:
self.db.rollback()
return False
return True
def _normalize_source(source: str | FeishuEventSource) -> FeishuEventSource:
return FeishuEventSource(source)
def _event_identity(
payload: dict[str, Any],
source: str | FeishuEventSource,
) -> dict[str, str | None] | None:
source_value = _normalize_source(source)
header = payload.get(FeishuPayloadKey.HEADER) or {}
event = payload.get(FeishuPayloadKey.EVENT) or {}
message = event.get(FeishuPayloadKey.MESSAGE) or {}
event_id = header.get(FeishuPayloadKey.EVENT_ID)
message_id = message.get(FeishuPayloadKey.MESSAGE_ID)
stable_id = event_id or message_id
if not stable_id:
return None
event_type = header.get(FeishuPayloadKey.EVENT_TYPE)
event_key = ":".join(
str(part)
for part in (source_value, event_type or FeishuPayloadKey.EVENT, stable_id)
)
return {
FeishuEventReceiptKey.EVENT_KEY: event_key,
FeishuEventReceiptKey.SOURCE: source_value,
FeishuEventReceiptKey.EVENT_ID: str(event_id) if event_id else None,
FeishuEventReceiptKey.MESSAGE_ID: str(message_id) if message_id else None,
}
def _approval_action_value(payload: dict[str, Any]) -> dict[str, Any]:
action = payload.get("action") or {}
event = payload.get(FeishuPayloadKey.EVENT) or {}
event_action = event.get("action") or {}
value = action.get("value") or event_action.get("value") or payload.get("value") or {}
if isinstance(value, str):
try:
parsed = json.loads(value)
except json.JSONDecodeError:
return {}
return parsed if isinstance(parsed, dict) else {}
return value if isinstance(value, dict) else {}
def _approval_operator(payload: dict[str, Any]) -> str:
operator = payload.get("operator") or (payload.get(FeishuPayloadKey.EVENT) or {}).get(
"operator"
) or {}
operator_id = operator.get("operator_id") or {}
return (
operator_id.get(FeishuPayloadKey.OPEN_ID)
or operator_id.get(FeishuPayloadKey.USER_ID)
or operator.get(FeishuPayloadKey.OPEN_ID)
or operator.get(FeishuPayloadKey.USER_ID)
or ActorValue.FEISHU
)
def _approval_event_identity(
payload: dict[str, Any],
ticket_id: str,
decision: str,
actor: str,
) -> dict[str, str | None]:
header = payload.get(FeishuPayloadKey.HEADER) or {}
event_id = header.get(FeishuPayloadKey.EVENT_ID)
stable_id = event_id or f"{ticket_id}:{decision}:{actor}"
return {
FeishuEventReceiptKey.EVENT_KEY: f"{FeishuEventSource.WEBHOOK}:approval:{stable_id}",
FeishuEventReceiptKey.SOURCE: FeishuEventSource.WEBHOOK,
FeishuEventReceiptKey.EVENT_ID: str(event_id) if event_id else None,
FeishuEventReceiptKey.MESSAGE_ID: None,
}