ForcePilot/backend/package/yuxi/channels/adapters/imessage/audit.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

91 lines
3.0 KiB
Python

from __future__ import annotations
import json
import time
from pathlib import Path
from typing import Any
from yuxi.utils.logging_config import logger
class AuditLogger:
def __init__(self, log_dir: str | None = None):
self._log_dir = Path(log_dir or str(Path.home() / ".yuxi" / "imessage" / "audit"))
self._log_dir.mkdir(parents=True, exist_ok=True)
self._buffer: list[dict[str, Any]] = []
def record(
self,
event: str,
channel_chat_id: str = "",
channel_user_id: str = "",
decision: str = "",
reason: str = "",
metadata: dict[str, Any] | None = None,
) -> None:
entry = {
"timestamp": time.time(),
"event": event,
"channel_chat_id": channel_chat_id,
"channel_user_id": channel_user_id,
"decision": decision,
"reason": reason,
"metadata": metadata or {},
}
self._buffer.append(entry)
logger.info(f"[iMessage/Audit] {event}: {decision}{reason}")
def record_dm_access(self, sender: str, allowed: bool, reason: str = "") -> None:
self.record(
event="dm_access",
channel_user_id=sender,
decision="allowed" if allowed else "blocked",
reason=reason,
)
def record_group_access(self, chat_guid: str, allowed: bool, reason: str = "") -> None:
self.record(
event="group_access",
channel_chat_id=chat_guid,
decision="allowed" if allowed else "blocked",
reason=reason,
)
def record_pairing(self, code: str, action: str, sender: str) -> None:
self.record(
event="pairing",
channel_user_id=sender,
decision=action,
reason=f"code={code}",
)
async def flush(self) -> None:
if not self._buffer:
return
date_str = time.strftime("%Y-%m-%d")
file_path = self._log_dir / f"audit-{date_str}.jsonl"
with file_path.open("a", encoding="utf-8") as f:
for entry in self._buffer:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
count = len(self._buffer)
self._buffer.clear()
logger.debug(f"[iMessage/Audit] Flushed {count} audit entries")
def get_recent(self, limit: int = 100) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
files = sorted(self._log_dir.glob("audit-*.jsonl"), reverse=True)
for fp in files:
if len(result) >= limit:
break
try:
lines = fp.read_text(encoding="utf-8").strip().split("\n")
for line in reversed(lines):
if not line.strip():
continue
result.append(json.loads(line))
if len(result) >= limit:
break
except (json.JSONDecodeError, OSError):
continue
return result[:limit]