WechatOnCloud/bridge/woc_bridge/models/contact.py
Kris c45282f094 feat: 新增好友自动通过、UI自动化能力与多分辨率适配
- 新增登录状态守卫后台任务
- 新增好友申请自动通过规则引擎
- 新增多分辨率UI配置与模板资源
- 新增消息拉取复合游标支持
- 优化发送队列与UI自动化逻辑
- 新增批量发送日志与错误处理
- 优化Docker镜像构建与ptrace初始化
- 新增联系人名称缓存预热
2026-07-17 18:10:31 +08:00

321 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""联系人域模型:联系人 / 联系人列表 / 群聊列表 / 群成员。"""
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