ForcePilot/backend/package/yuxi/channels/adapters/imessage/conversation_bindings.py
Kris 068cf70fe9 feat(imessage): 实现完整的 iMessage 适配器基础模块
新增 iMessage 通道适配器完整实现,包含:
1. 核心适配器与工具工厂导出
2. 运行时存储、反射防护、会话路由等基础组件
3. 消息信封、线程管理、回复上下文格式化
4. Tapback 表情反应处理、自定义异常体系
5. 审批按钮、联系人解析、速率限制功能
6. 文本净化、目标解析、缓存管理模块
7. 配置 schema、多账户支持、安装向导等配置模块
8. 审计日志、媒体AI处理等扩展功能
2026-05-12 00:44:44 +08:00

89 lines
3.3 KiB
Python

from __future__ import annotations
import json
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from yuxi.utils.logging_config import logger
@dataclass
class ConversationBinding:
channel_chat_id: str
agent_id: str = "main"
label: str = ""
created_at: float = field(default_factory=time.time)
metadata: dict[str, Any] = field(default_factory=dict)
class ConversationBindingManager:
def __init__(self, storage_dir: str | None = None, account_id: str = "default"):
self._bindings: dict[str, ConversationBinding] = {}
self._storage_dir = storage_dir or str(Path.home() / ".yuxi" / "imessage")
self._account_id = account_id
def create(
self, channel_chat_id: str, agent_id: str = "main", label: str = "", metadata: dict[str, Any] | None = None
) -> ConversationBinding:
binding = ConversationBinding(
channel_chat_id=channel_chat_id,
agent_id=agent_id,
label=label,
metadata=metadata or {},
)
self._bindings[channel_chat_id] = binding
logger.info(f"[iMessage/Bindings] Created binding: {channel_chat_id} -> agent:{agent_id}")
return binding
def get(self, channel_chat_id: str) -> ConversationBinding | None:
return self._bindings.get(channel_chat_id)
def list_all(self) -> list[ConversationBinding]:
return list(self._bindings.values())
def delete(self, channel_chat_id: str) -> bool:
if channel_chat_id in self._bindings:
del self._bindings[channel_chat_id]
logger.info(f"[iMessage/Bindings] Deleted binding: {channel_chat_id}")
return True
return False
def get_agent_for_chat(self, channel_chat_id: str, default: str = "main") -> str:
binding = self._bindings.get(channel_chat_id)
return binding.agent_id if binding else default
async def save_bindings(self) -> None:
dir_path = Path(self._storage_dir) / self._account_id
dir_path.mkdir(parents=True, exist_ok=True)
data = {
cid: {
"agent_id": b.agent_id,
"label": b.label,
"created_at": b.created_at,
"metadata": b.metadata,
}
for cid, b in self._bindings.items()
}
file_path = dir_path / "conversation-bindings.json"
file_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
async def load_bindings(self) -> None:
file_path = Path(self._storage_dir) / self._account_id / "conversation-bindings.json"
if not file_path.exists():
return
try:
data = json.loads(file_path.read_text(encoding="utf-8"))
for cid, info in data.items():
if isinstance(info, dict):
self._bindings[cid] = ConversationBinding(
channel_chat_id=cid,
agent_id=info.get("agent_id", "main"),
label=info.get("label", ""),
created_at=info.get("created_at", time.time()),
metadata=info.get("metadata", {}),
)
except (json.JSONDecodeError, OSError) as e:
logger.warning(f"[iMessage/Bindings] Failed to load bindings: {e}")