新增 iMessage 通道适配器完整实现,包含: 1. 核心适配器与工具工厂导出 2. 运行时存储、反射防护、会话路由等基础组件 3. 消息信封、线程管理、回复上下文格式化 4. Tapback 表情反应处理、自定义异常体系 5. 审批按钮、联系人解析、速率限制功能 6. 文本净化、目标解析、缓存管理模块 7. 配置 schema、多账户支持、安装向导等配置模块 8. 审计日志、媒体AI处理等扩展功能
91 lines
3.0 KiB
Python
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]
|