ForcePilot/backend/package/yuxi/channels/adapters/googlechat/pairing.py
Kris 87fc26cd15 refactor(googlechat): 整理并优化Google Chat适配器代码
- 调整依赖导入顺序与清理冗余空行
- 修复setup wizard步骤存储逻辑
- 新增消息编辑/转发标记、卡片控件支持
- 完善目标校验、媒体上传、配对存储功能
- 优化流式响应与错误处理逻辑
- 增强SSRF防护与配置灵活性
- 新增/扩展内置命令与卡片处理能力
- 调整媒体大小限制与类型参数
2026-05-13 16:09:31 +08:00

121 lines
3.8 KiB
Python

from __future__ import annotations
import json
import os
import threading
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from googleapiclient.discovery import Resource
from yuxi.channels.models import DeliveryResult
from yuxi.utils.logging_config import logger
_PAIRING_CHALLENGE_MESSAGE = (
"{agent_name} Approval Required\n\n"
"To continue, send the following:\n"
"```\n/approve {user_id}\n```\n"
"This confirmation is required to pair with this bot account."
)
class PairingStore:
def __init__(self, storage_path: str = ""):
self._paired_users: dict[str, str] = {}
self._lock = threading.Lock()
self._storage_path = storage_path or os.path.join(
os.path.expanduser("~"), ".forcepilot", "googlechat_pairing.json"
)
self._load()
def _load(self) -> None:
try:
if os.path.exists(self._storage_path):
with open(self._storage_path, encoding="utf-8") as f:
self._paired_users = json.load(f)
logger.debug(f"Loaded {len(self._paired_users)} paired users from {self._storage_path}")
except Exception as e:
logger.warning(f"Failed to load pairing store: {e}")
self._paired_users = {}
def _save(self) -> None:
try:
os.makedirs(os.path.dirname(self._storage_path), exist_ok=True)
with open(self._storage_path, "w", encoding="utf-8") as f:
json.dump(self._paired_users, f, indent=2)
except Exception as e:
logger.warning(f"Failed to save pairing store: {e}")
def is_paired(self, space_id: str) -> bool:
with self._lock:
return space_id in self._paired_users
def pair(self, space_id: str, user_id: str) -> None:
with self._lock:
self._paired_users[space_id] = user_id
self._save()
def unpair(self, space_id: str) -> bool:
with self._lock:
if space_id in self._paired_users:
del self._paired_users[space_id]
self._save()
return True
return False
def get_paired_user(self, space_id: str) -> str | None:
with self._lock:
return self._paired_users.get(space_id)
def list_paired_spaces(self) -> list[str]:
with self._lock:
return list(self._paired_users.keys())
def clear(self) -> None:
with self._lock:
self._paired_users.clear()
self._save()
_global_pairing_store: PairingStore | None = None
def get_pairing_store(storage_path: str = "") -> PairingStore:
global _global_pairing_store
if _global_pairing_store is None:
_global_pairing_store = PairingStore(storage_path)
return _global_pairing_store
async def send_pairing_challenge(
chat_service: Resource,
space_name: str,
user_id: str,
agent_name: str = "Bot",
) -> DeliveryResult:
content = _PAIRING_CHALLENGE_MESSAGE.format(
agent_name=agent_name,
user_id=user_id,
)
body = {"text": content}
try:
result = chat_service.spaces().messages().create(parent=space_name, body=body).execute()
msg_id = result.get("name", "")
logger.info(f"Pairing challenge sent to {space_name}: user_id={user_id}")
return DeliveryResult(success=True, message_id=msg_id)
except Exception as e:
logger.warning(f"Failed to send pairing challenge to {space_name}: {e}")
return DeliveryResult(success=False, error=str(e))
async def check_pairing_approval(
chat_service: Resource,
space_name: str,
user_id: str,
message_text: str,
) -> bool:
approved = f"/approve {user_id}" in message_text.strip().lower()
if approved:
logger.info(f"Pairing approved for {space_name}: user_id={user_id}")
return approved