ForcePilot/backend/package/yuxi/channel/extensions/googlechat/monitor.py
Kris c4e301a5e0 feat(googlechat): 新增Google Chat渠道完整实现
新增Google Chat插件的所有核心功能模块,包括账户配置、消息收发、权限控制、媒体处理、会话管理等完整能力
2026-05-21 10:49:33 +08:00

170 lines
5.5 KiB
Python

from __future__ import annotations
import logging
from datetime import datetime, timezone
from yuxi.channel.extensions.googlechat.formatting import sanitize_text
from yuxi.channel.extensions.googlechat.media import build_attachment_placeholder
from yuxi.channel.extensions.googlechat.types import GoogleChatEvent, ResolvedGoogleChatAccount
from yuxi.channel.message.models import (
GroupContext,
MessageType,
PeerInfo,
UnifiedMessage,
)
from yuxi.channel.routing.models import PeerKind
logger = logging.getLogger(__name__)
class GoogleChatMonitor:
def __init__(self):
self._last_event_time: float = 0
def event_to_unified_message(
self,
raw_payload: dict,
event: GoogleChatEvent,
text: str | None = None,
account: ResolvedGoogleChatAccount | None = None,
) -> UnifiedMessage | None:
if not event.message:
return None
msg = event.message
sender = msg.sender
space_name = event.effective_space_name
space_type = event.space_type
safe_text = text if text is not None else sanitize_text(msg.argument_text or msg.text)
if not safe_text:
return None
is_dm = space_type == "DM" or event.effective_space.single_user_bot_dm if event.effective_space else False
is_group = space_type in ("GROUP_CHAT", "SPACE") and not is_dm
sender_kind = PeerKind.DIRECT if is_dm else PeerKind.GROUP
sender_info = PeerInfo(
kind=sender_kind,
id=sender.name or "unknown",
display_name=sender.display_name,
username=sender.email,
is_bot=sender.type == "BOT",
)
group = None
if is_group:
group = GroupContext(
id=space_name,
name=event.effective_space.display_name if event.effective_space else None,
kind="group",
)
thread_id = None
if msg.thread:
thread_id = msg.thread.get("name")
timestamp = None
if event.event_time:
try:
from datetime import datetime as dt
timestamp = dt.fromisoformat(event.event_time.replace("Z", "+00:00"))
except (ValueError, TypeError):
pass
media_urls = []
media_types = []
for att in msg.attachment:
dr = att.get("driveDataRef") or att.get("attachmentDataRef")
if dr:
media_urls.append(dr.get("resourceName", ""))
media_types.append(att.get("contentType", ""))
if msg.attachment:
placeholder = build_attachment_placeholder(msg.attachment)
if placeholder:
safe_text = f"{placeholder}\n{safe_text}"
was_mentioned = _detect_mention(msg.annotations)
slash_cmd = _detect_slash_command(msg.annotations)
metadata = {
"ChatType": "direct" if is_dm else "group",
"WasMentioned": was_mentioned,
"CommandAuthorized": True,
"space_type": space_type,
"space_name": space_name,
}
if slash_cmd:
metadata["SlashCommand"] = slash_cmd
if is_group and event.effective_space:
metadata["GroupSpace"] = event.effective_space.display_name
return UnifiedMessage(
msg_id=msg.name or f"gc:{int(datetime.now(tz=timezone.utc).timestamp())}",
channel_type="googlechat",
account_id=account.account_id if account else "default",
content=safe_text,
sender=sender_info,
message_type=MessageType.TEXT,
media_urls=media_urls,
media_types=media_types,
group=group,
timestamp=timestamp,
raw_payload=raw_payload,
message_thread_id=thread_id,
metadata=metadata,
was_mentioned=was_mentioned,
)
async def inject_thread_context(
self,
account: ResolvedGoogleChatAccount,
space_name: str,
thread_name: str,
) -> str | None:
try:
from yuxi.channel.extensions.googlechat.api import list_thread_messages
from yuxi.channel.extensions.googlechat.formatting import format_thread_context
messages = await list_thread_messages(account, space_name, thread_name, page_size=20)
if not messages:
return None
history = []
for msg in messages:
sender = msg.get("sender", {})
history.append({
"author": sender.get("displayName", sender.get("name", "Unknown")),
"content": msg.get("text", msg.get("argumentText", "")),
})
return format_thread_context(history)
except Exception:
logger.debug("Failed to fetch thread history for %s in %s", thread_name, space_name)
return None
def _detect_mention(annotations: list[dict]) -> bool:
if not annotations:
return False
for ann in annotations:
if ann.get("type") == "USER_MENTION":
um = ann.get("userMention", {})
mention_user = um.get("user", {})
if mention_user.get("name") == "users/app":
return True
return False
def _detect_slash_command(annotations: list[dict]) -> str | None:
if not annotations:
return None
for ann in annotations:
if ann.get("type") == "SLASH_COMMAND":
sc = ann.get("slashCommand", {})
return sc.get("commandId")
return None