- 调整依赖导入顺序与清理冗余空行 - 修复setup wizard步骤存储逻辑 - 新增消息编辑/转发标记、卡片控件支持 - 完善目标校验、媒体上传、配对存储功能 - 优化流式响应与错误处理逻辑 - 增强SSRF防护与配置灵活性 - 新增/扩展内置命令与卡片处理能力 - 调整媒体大小限制与类型参数
121 lines
3.8 KiB
Python
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
|