ForcePilot/backend/package/yuxi/channels/adapters/feishu/session.py
Kris 1f78c44b03 refactor: 整理并清理项目中的冗余代码与格式问题
这是一个批量整理提交,包含以下主要改动:
1.  删除多处冗余的空行和未使用的导入
2.  修复文件末尾缺少换行符的问题
3.  调整部分模块的导入顺序与代码排版
4.  修复部分配置默认值与策略逻辑
5.  新增多个功能模块与辅助工具
6.  完善异常处理与日志记录
7.  修复速率限制、消息缓存、权限校验等逻辑bug
8.  废弃部分旧有API与配置项并添加警告提示
2026-05-12 14:51:53 +08:00

184 lines
5.7 KiB
Python

from __future__ import annotations
import json
import os
import time
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
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
class SessionPersistence:
def __init__(self, persist_path: str = ""):
self._persist_path = persist_path
self._data: dict[str, dict] = {}
if persist_path and os.path.exists(persist_path):
self._load()
def _load(self) -> None:
try:
with open(self._persist_path, encoding="utf-8") as f:
self._data = json.load(f)
except (json.JSONDecodeError, OSError):
self._data = {}
def _save(self) -> None:
if not self._persist_path:
return
try:
tmp = self._persist_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(self._data, f, ensure_ascii=False)
os.replace(tmp, self._persist_path)
except OSError:
pass
def get(self, session_id: str, key: str) -> str | None:
session = self._data.get(session_id, {})
return session.get(key)
def set(self, session_id: str, key: str, value: str) -> None:
if session_id not in self._data:
self._data[session_id] = {}
self._data[session_id][key] = value
self._save()
def delete(self, session_id: str, key: str = "") -> None:
if key:
session = self._data.get(session_id, {})
session.pop(key, None)
if not session:
self._data.pop(session_id, None)
else:
self._data.pop(session_id, None)
self._save()
def cleanup_expired(self, max_age_s: float = 86400) -> None:
now = time.time()
expired = []
for sid, data in self._data.items():
last_active = data.get("_last_active", 0)
if isinstance(last_active, (int, float)) and now - last_active > max_age_s:
expired.append(sid)
for sid in expired:
self._data.pop(sid, None)
if expired:
self._save()