This commit is contained in:
Yingwen Luo-LUOYW
2026-02-23 09:41:56 +08:00
15 changed files with 608 additions and 73 deletions

View File

@@ -304,7 +304,8 @@ class EmailChannel(BaseChannel):
self._processed_uids.add(uid)
# mark_seen is the primary dedup; this set is a safety net
if len(self._processed_uids) > self._MAX_PROCESSED_UIDS:
self._processed_uids.clear()
# Evict a random half to cap memory; mark_seen is the primary dedup
self._processed_uids = set(list(self._processed_uids)[len(self._processed_uids) // 2:])
if mark_seen:
client.store(imap_id, "+FLAGS", "\\Seen")

View File

@@ -503,18 +503,29 @@ class FeishuChannel(BaseChannel):
logger.error("Error downloading image {}: {}", image_key, e)
return None, None
def _download_file_sync(self, file_key: str) -> tuple[bytes | None, str | None]:
"""Download a file from Feishu by file_key."""
def _download_file_sync(
self, message_id: str, file_key: str, resource_type: str = "file"
) -> tuple[bytes | None, str | None]:
"""Download a file/audio/media from a Feishu message by message_id and file_key."""
try:
request = GetFileRequest.builder().file_key(file_key).build()
response = self._client.im.v1.file.get(request)
request = (
GetMessageResourceRequest.builder()
.message_id(message_id)
.file_key(file_key)
.type(resource_type)
.build()
)
response = self._client.im.v1.message_resource.get(request)
if response.success():
return response.file, response.file_name
file_data = response.file
if hasattr(file_data, "read"):
file_data = file_data.read()
return file_data, response.file_name
else:
logger.error("Failed to download file: code={}, msg={}", response.code, response.msg)
logger.error("Failed to download {}: code={}, msg={}", resource_type, response.code, response.msg)
return None, None
except Exception as e:
logger.error("Error downloading file {}: {}", file_key, e)
except Exception:
logger.exception("Error downloading {} {}", resource_type, file_key)
return None, None
async def _download_and_save_media(
@@ -544,14 +555,14 @@ class FeishuChannel(BaseChannel):
if not filename:
filename = f"{image_key[:16]}.jpg"
elif msg_type in ("audio", "file"):
elif msg_type in ("audio", "file", "media"):
file_key = content_json.get("file_key")
if file_key:
if file_key and message_id:
data, filename = await loop.run_in_executor(
None, self._download_file_sync, file_key
None, self._download_file_sync, message_id, file_key, msg_type
)
if not filename:
ext = ".opus" if msg_type == "audio" else ""
ext = {"audio": ".opus", "media": ".mp4"}.get(msg_type, "")
filename = f"{file_key[:16]}{ext}"
if data and filename:
@@ -684,7 +695,7 @@ class FeishuChannel(BaseChannel):
if text:
content_parts.append(text)
elif msg_type in ("image", "audio", "file"):
elif msg_type in ("image", "audio", "file", "media"):
file_path, content_text = await self._download_and_save_media(msg_type, content_json, message_id)
if file_path:
media_paths.append(file_path)

View File

@@ -55,7 +55,6 @@ class QQChannel(BaseChannel):
self.config: QQConfig = config
self._client: "botpy.Client | None" = None
self._processed_ids: deque = deque(maxlen=1000)
self._bot_task: asyncio.Task | None = None
async def start(self) -> None:
"""Start the QQ bot."""
@@ -71,8 +70,8 @@ class QQChannel(BaseChannel):
BotClass = _make_bot_class(self)
self._client = BotClass()
self._bot_task = asyncio.create_task(self._run_bot())
logger.info("QQ bot started (C2C private message)")
await self._run_bot()
async def _run_bot(self) -> None:
"""Run the bot connection with auto-reconnect."""
@@ -88,11 +87,10 @@ class QQChannel(BaseChannel):
async def stop(self) -> None:
"""Stop the QQ bot."""
self._running = False
if self._bot_task:
self._bot_task.cancel()
if self._client:
try:
await self._bot_task
except asyncio.CancelledError:
await self._client.close()
except Exception:
pass
logger.info("QQ bot stopped")
@@ -130,5 +128,5 @@ class QQChannel(BaseChannel):
content=content,
metadata={"message_id": data.id},
)
except Exception as e:
logger.error("Error handling QQ message: {}", e)
except Exception:
logger.exception("Error handling QQ message")

View File

@@ -179,18 +179,21 @@ class SlackChannel(BaseChannel):
except Exception as e:
logger.debug("Slack reactions_add failed: {}", e)
await self._handle_message(
sender_id=sender_id,
chat_id=chat_id,
content=text,
metadata={
"slack": {
"event": event,
"thread_ts": thread_ts,
"channel_type": channel_type,
}
},
)
try:
await self._handle_message(
sender_id=sender_id,
chat_id=chat_id,
content=text,
metadata={
"slack": {
"event": event,
"thread_ts": thread_ts,
"channel_type": channel_type,
}
},
)
except Exception:
logger.exception("Error handling Slack message from {}", sender_id)
def _is_allowed(self, sender_id: str, chat_id: str, channel_type: str) -> bool:
if channel_type == "im":