ForcePilot/backend/package/yuxi/channels/adapters/feishu/session.py
Kris a6fa7245e5 feat(feishu): 完整实现飞书适配器核心模块
新增飞书机器人适配器全套功能,包括:
- 基础适配器入口与工具导出
- 消息格式化、卡片渲染、回复调度逻辑
- 会话ID生成、模型覆盖策略
- 消息发送缓存、顺序队列管理
- 飞书签名验证、加解密webhook请求
- 审批权限校验、机器人菜单事件处理
- 文档评论、钉消息、语音转码处理
- 静态/动态目录管理、子代理生命周期管理
- 各类工具集:聊天、云盘、文档、知识库API封装
2026-05-12 00:43:59 +08:00

124 lines
3.8 KiB
Python

from __future__ import annotations
from enum import StrEnum
CHAT_ID_PREFIX_DM = "feishu:dm:"
CHAT_ID_PREFIX_GROUP = "feishu:group:"
CHAT_ID_PREFIX_THREAD = "feishu:thread:"
CHAT_ID_PREFIX_GROUP_SENDER = "feishu:group_sender:"
CHAT_ID_PREFIX_GROUP_TOPIC = "feishu:group_topic:"
CHAT_ID_PREFIX_GROUP_TOPIC_SENDER = "feishu:group_topic_sender:"
GroupSessionScope = str # "group" | "group_sender" | "group_topic" | "group_topic_sender"
class SessionMode(StrEnum):
RAW = "raw"
THREAD = "thread"
CHAT_RAW = "chat_raw"
CHAT_RESOLVE = "chat_resolve"
def generate_feishu_chat_id(
raw_chat_id: str,
chat_type: str,
*,
root_id: str | None = None,
sender_id: str | None = None,
mode: SessionMode = SessionMode.CHAT_RESOLVE,
scope: GroupSessionScope = "group",
) -> str:
if mode == SessionMode.RAW:
return raw_chat_id
if mode == SessionMode.CHAT_RAW:
prefix = f"feishu:{chat_type}:"
return f"{prefix}{raw_chat_id}"
if mode == SessionMode.THREAD and root_id:
return f"{CHAT_ID_PREFIX_THREAD}{root_id}"
if chat_type in ("group", "thread") and root_id:
if scope == "group_topic_sender" and sender_id:
return f"{CHAT_ID_PREFIX_GROUP_TOPIC_SENDER}{root_id}:{raw_chat_id}:{sender_id}"
if scope == "group_topic":
return f"{CHAT_ID_PREFIX_GROUP_TOPIC}{root_id}:{raw_chat_id}"
if scope == "group_sender" and sender_id:
return f"{CHAT_ID_PREFIX_GROUP_SENDER}{raw_chat_id}:{sender_id}"
return f"{CHAT_ID_PREFIX_GROUP}{root_id}:{raw_chat_id}"
if chat_type == "group" or chat_type == "direct" or chat_type == "p2p":
if scope == "group_sender" and sender_id:
prefix = CHAT_ID_PREFIX_DM if (chat_type == "direct" or chat_type == "p2p") else CHAT_ID_PREFIX_GROUP_SENDER
return f"{prefix}{raw_chat_id}:{sender_id}"
prefix = CHAT_ID_PREFIX_DM if (chat_type == "direct" or chat_type == "p2p") else CHAT_ID_PREFIX_GROUP
return f"{prefix}{raw_chat_id}"
return f"{CHAT_ID_PREFIX_DM}{raw_chat_id}"
def resolve_feishu_chat_type(raw_chat_id: str) -> str:
return "direct" if raw_chat_id.startswith("ou_") else "group"
def resolve_feishu_command_conversation(
chat_id: str,
user_id: str,
*,
scope: GroupSessionScope = "group",
) -> str:
if user_id.startswith("ou_"):
return f"{CHAT_ID_PREFIX_DM}{user_id}"
return generate_feishu_chat_id(chat_id, "group", scope=scope)
def normalize_feishu_acp_conversation_id(conv_id: str) -> str:
if conv_id.startswith("feishu:"):
return conv_id
if conv_id.startswith("ou_"):
return f"{CHAT_ID_PREFIX_DM}{conv_id}"
if conv_id.startswith("oc_"):
return f"{CHAT_ID_PREFIX_GROUP}{conv_id}"
return conv_id
def match_feishu_acp_conversation(conv_id: str, pattern: str) -> bool:
import fnmatch
normalized = normalize_feishu_acp_conversation_id(conv_id)
return fnmatch.fnmatch(normalized, pattern)
_FEISHU_ID_PREFIXES = ("ou_", "oc_", "on_", "om_", "od_", "og_", "tg_")
def looks_like_feishu_id(value: str) -> bool:
if not value or not isinstance(value, str):
return False
return any(value.startswith(p) for p in _FEISHU_ID_PREFIXES)
def normalize_feishu_target(target: str) -> str:
if not target or not isinstance(target, str):
return target
target = target.strip()
if target.startswith("feishu:"):
return target
if target.startswith("lark:"):
return "feishu:" + target[5:]
if target.startswith("feishu://"):
return "feishu:" + target[9:]
for prefix in _FEISHU_ID_PREFIXES:
if target.startswith(prefix):
if prefix in ("ou_", "on_"):
return f"{CHAT_ID_PREFIX_DM}{target}"
if prefix in ("oc_", "om_", "od_", "og_"):
return f"{CHAT_ID_PREFIX_GROUP}{target}"
return target