"""联系人域模型:联系人 / 联系人列表 / 群聊列表 / 群成员。""" from __future__ import annotations import asyncio import enum import logging from typing import Optional from pydantic import BaseModel, Field logger = logging.getLogger("woc-bridge") # --------------------------------------------------------------------------- # 联系人接口 # --------------------------------------------------------------------------- class Contact(BaseModel): """联系人。""" wxid: str nickname: str = "" remark: str = "" avatar_url: str = "" type: str = Field( default="person", description="person / group / official / system", ) # 扩展字段:从 contact.db 的 contact 表读取 alias: Optional[str] = Field(default=None, description="微信号/别名") encrypt_username: Optional[str] = Field(default=None, description="加密用户名") quan_pin: Optional[str] = Field(default=None, description="全拼") pin_yin_initial: Optional[str] = Field(default=None, description="拼音首字母") big_head_url: Optional[str] = Field(default=None, description="高清头像 URL") small_head_url: Optional[str] = Field(default=None, description="缩略头像 URL") description: Optional[str] = Field(default=None, description="个性签名/描述") local_type: Optional[int] = Field(default=None, description="联系人类型标记") verify_flag: Optional[int] = Field(default=None, description="认证标记") delete_flag: Optional[int] = Field(default=None, description="删除标记") chat_room_type: Optional[int] = Field(default=None, description="群类型标记") class ContactsResponse(BaseModel): """联系人列表响应。""" contacts: list[Contact] = Field(default_factory=list) total: int = 0 class GroupsResponse(BaseModel): """群聊列表响应。""" groups: list[Contact] = Field(default_factory=list) total: int = 0 # --------------------------------------------------------------------------- # 群成员接口 # --------------------------------------------------------------------------- class GroupMember(BaseModel): """群成员。""" wxid: str nickname: str = "" display_name: str = "" is_admin: bool = False class GroupMembersResponse(BaseModel): """群成员列表响应。""" group_wxid: str members: list[GroupMember] = Field(default_factory=list) total: int = 0 # --------------------------------------------------------------------------- # 修改好友备注(experimental) # --------------------------------------------------------------------------- class SetRemarkRequest(BaseModel): """修改好友备注请求(experimental)。""" remark: str = Field(description="新备注名") display_name: Optional[str] = Field( default=None, description="会话显示名(备注/昵称)用于搜索框定位;" "不传则从 contact.db 解析(修改前的 remark 优先,其次 nickname,回退 wxid)", ) class SetRemarkResponse(BaseModel): """修改好友备注响应。""" success: bool = Field(default=False) error: Optional[str] = Field(default=None, description="失败时的错误描述") verified: Optional[bool] = Field( default=None, description="post_verify 校验结果:True=DB 中 remark 已更新为目标值," "False=超时仍为旧值,None=未执行校验", ) # --------------------------------------------------------------------------- # 添加好友(experimental) # --------------------------------------------------------------------------- class AddFriendRequest(BaseModel): """添加好友请求(experimental)。""" keyword: str = Field(description="搜索关键词(wxid/手机号/微信号)") message: str = Field(default="", description="可选好友验证消息") class AddFriendResponse(BaseModel): """添加好友响应。""" success: bool = Field(default=False) error: Optional[str] = Field(default=None, description="失败时的错误描述") verified: Optional[bool] = Field( default=None, description="post_verify 校验结果:True=检测到好友列表变化或输入框清空," "False=未检测到变化,None=未执行校验", ) # --------------------------------------------------------------------------- # 好友申请自动通过 # --------------------------------------------------------------------------- class FriendRequestItem(BaseModel): """好友申请条目(API 响应用)。""" stranger_wxid: str nickname: str = "" verify_message: str = "" scene: str = "" create_time: int # 运行时数据结构:避免 friend_parser.py 与 friend_watcher.py 循环导入 class FriendRequestInfo: """好友申请信息(从 fmessage sysmsg XML 解析)。""" def __init__( self, stranger_wxid: str, nickname: str, verify_message: str, scene: str, raw_xml: str, create_time: int, msg_local_id: int, ) -> None: self.stranger_wxid = stranger_wxid self.nickname = nickname self.verify_message = verify_message self.scene = scene self.raw_xml = raw_xml self.create_time = create_time self.msg_local_id = msg_local_id def __repr__(self) -> str: return ( f"FriendRequestInfo(stranger_wxid={self.stranger_wxid!r}, " f"nickname={self.nickname!r}, scene={self.scene!r}, " f"create_time={self.create_time}, msg_local_id={self.msg_local_id})" ) class FriendRequestsResponse(BaseModel): """好友申请列表响应。""" requests: list[FriendRequestItem] = Field(default_factory=list) total: int = 0 class AcceptFriendRequest(BaseModel): """手动通过好友申请请求。""" stranger_wxid: str = Field(description="申请人 wxid(含 @stranger 后缀)") nickname: Optional[str] = Field(default=None, description="申请人昵称(UI 定位用)") class AcceptFriendResponse(BaseModel): """通过好友申请响应。""" success: bool = Field(default=False) error: Optional[str] = Field(default=None) verified: Optional[bool] = Field( default=None, description="post_verify 校验结果:True=DB 中 @stranger 后缀消失已变为好友," "False=超时仍未通过,None=未执行校验", ) class AutoAcceptStatus(BaseModel): """自动通过运行状态。""" running: bool enabled: bool processed_count: int = 0 accepted_count: int = 0 rejected_count: int = 0 last_processed_time: Optional[int] = None cursor_create_time: Optional[int] = Field( default=None, description="当前轮询 create_time 游标(调试用)" ) cursor_local_id: Optional[int] = Field( default=None, description="当前轮询 local_id 游标(调试用)" ) db_readable: Optional[bool] = Field( default=None, description="DB 是否可读(调试用)" ) breaker_state: Optional[str] = Field( default=None, description="熔断器当前状态(调试用)" ) class AcceptRuleConfig(BaseModel): """自动通过规则配置。""" enabled: bool = Field(default=False, description="全局开关") accept_all: bool = Field(default=False, description="通过所有申请(忽略以下规则)") whitelist_wxids: list[str] = Field(default_factory=list, description="白名单 wxid 列表") whitelist_nicknames: list[str] = Field(default_factory=list, description="白名单昵称列表(精确匹配)") keywords: list[str] = Field(default_factory=list, description="验证消息关键词列表(子串匹配)") blacklist_wxids: list[str] = Field(default_factory=list, description="黑名单 wxid 列表") blacklist_nicknames: list[str] = Field(default_factory=list, description="黑名单昵称列表") allow_scenes: list[str] = Field(default_factory=list, description="允许的场景(空=不限制)") class AcceptDecision(enum.Enum): """规则匹配决策结果。""" ACCEPT = "accept" REJECT = "reject" SKIP = "skip" class AcceptRuleEngine: """好友申请自动通过规则引擎。 按"黑名单 → 场景 → 白名单 → 关键词 → SKIP"优先级决策, 配置热更新受 asyncio.Lock 保护。 """ def __init__(self, config: AcceptRuleConfig) -> None: self._config = config self._lock = asyncio.Lock() async def get_config(self) -> AcceptRuleConfig: """获取当前规则配置(供 API 查询)。""" async with self._lock: return self._config async def evaluate(self, req: "FriendRequestInfo") -> AcceptDecision: async with self._lock: cfg = self._config logger.info( "[规则引擎] 开始评估 wxid=%s nickname=%s scene=%s verify=%s", req.stranger_wxid, req.nickname, req.scene, repr(req.verify_message[:80]) if req.verify_message else "", ) logger.info( "[规则引擎] 当前配置 enabled=%s accept_all=%s allow_scenes=%s keywords=%s " "whitelist_wxids=%s whitelist_nicknames=%s blacklist_wxids=%s blacklist_nicknames=%s", cfg.enabled, cfg.accept_all, cfg.allow_scenes, cfg.keywords, cfg.whitelist_wxids, cfg.whitelist_nicknames, cfg.blacklist_wxids, cfg.blacklist_nicknames, ) if not cfg.enabled: logger.info("[规则引擎] 全局开关 enabled=false → SKIP") return AcceptDecision.SKIP if cfg.accept_all: logger.info("[规则引擎] accept_all=true → ACCEPT") return AcceptDecision.ACCEPT # 黑名单优先 if req.stranger_wxid in cfg.blacklist_wxids: logger.info("[规则引擎] wxid 命中黑名单 → REJECT") return AcceptDecision.REJECT if req.nickname and req.nickname in cfg.blacklist_nicknames: logger.info("[规则引擎] nickname 命中黑名单 → REJECT") return AcceptDecision.REJECT logger.info("[规则引擎] 黑名单未命中") # 场景过滤 if cfg.allow_scenes and req.scene not in cfg.allow_scenes: logger.info( "[规则引擎] scene=%s 不在允许列表 %s → SKIP", req.scene, cfg.allow_scenes, ) return AcceptDecision.SKIP logger.info("[规则引擎] 场景检查通过 allow_scenes=%s", cfg.allow_scenes) # 白名单 if req.stranger_wxid in cfg.whitelist_wxids: logger.info("[规则引擎] wxid 命中白名单 → ACCEPT") return AcceptDecision.ACCEPT if req.nickname and req.nickname in cfg.whitelist_nicknames: logger.info("[规则引擎] nickname 命中白名单 → ACCEPT") return AcceptDecision.ACCEPT logger.info("[规则引擎] 白名单未命中") # 关键词 if cfg.keywords: for kw in cfg.keywords: if kw in req.verify_message: logger.info("[规则引擎] 验证消息命中关键词 '%s' → ACCEPT", kw) return AcceptDecision.ACCEPT logger.info( "[规则引擎] 关键词未命中 keywords=%s verify=%s", cfg.keywords, repr(req.verify_message[:80]) if req.verify_message else "", ) else: logger.info("[规则引擎] 未配置关键词") logger.info("[规则引擎] 无匹配规则 → SKIP") return AcceptDecision.SKIP async def update_config(self, config: AcceptRuleConfig) -> None: async with self._lock: self._config = config