diff --git a/config.contacts.yaml b/config.contacts.yaml new file mode 100644 index 0000000..8e35a4c --- /dev/null +++ b/config.contacts.yaml @@ -0,0 +1,26 @@ +# 联系人分类配置 +# 用于控制哪些联系人需要自动回复,哪些需要忽略 + +# 重点用户:开启自动回复(关键词 + AI) +priority: + enabled: true + users: + - "尾巴~" + # - "张三" + # - "李四" + # 支持模糊匹配(TODO):如 "张*" 匹配所有姓张的 + +# 忽略列表:不自动回复,手动处理 +ignore: + enabled: true + users: + - "相亲相爱一家人" + - "工作通知群" + - "文件传输助手" + # 公众号前缀匹配 + - "公众号:" + # 也可以用正则匹配(TODO) + +# 普通用户:默认不自动回复 +normal: + auto_reply: false # true = AI 回复,false = 不回复 diff --git a/config.example.yaml b/config.example.yaml index ae56d68..31ade59 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -50,7 +50,7 @@ rules: reply_type: keyword reply_content: "您好,有什么可以帮您的?" enabled: true - + # AI 回复模式(无匹配关键词时,使用 LLM 生成回复) # 这是默认模式,会根据对话上下文生成自然回复 - keywords: [] @@ -58,6 +58,32 @@ rules: reply_content: "" enabled: true +# ============================================ +# 联系人分类配置(Phase 1) +# ============================================ +# 用于控制哪些联系人需要自动回复,哪些需要忽略 +contacts: + # 重点用户:开启自动回复(关键词 + AI) + priority: + enabled: true + users: + - "尾巴~" + # - "张三" + # 支持模糊匹配(TODO) + + # 忽略列表:不自动回复,手动处理 + ignore: + enabled: true + users: + - "相亲相爱一家人" + - "工作通知群" + - "文件传输助手" + - "公众号:" # 所有公众号 + + # 普通用户:默认不自动回复 + normal: + auto_reply: false # true = AI 回复,false = 不回复 + # 知识库(可选,后续接入 OpenViking) knowledge_base: url: http://192.168.5.5:1933 diff --git a/src/config/__pycache__/settings.cpython-311.pyc b/src/config/__pycache__/settings.cpython-311.pyc index 9a4e19b..f3a434a 100644 Binary files a/src/config/__pycache__/settings.cpython-311.pyc and b/src/config/__pycache__/settings.cpython-311.pyc differ diff --git a/src/config/settings.py b/src/config/settings.py index 48ccfcc..9f2b767 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -49,6 +49,21 @@ class ReplyRule: enabled: bool = True +@dataclass +class ContactSettings: + """联系人分类配置""" + enabled: bool = True + users: List[str] = field(default_factory=list) + + +@dataclass +class ContactsConfig: + """联系人配置""" + priority: ContactSettings = field(default_factory=ContactSettings) + ignore: ContactSettings = field(default_factory=ContactSettings) + normal_auto_reply: bool = False # 普通用户是否开启 AI 回复 + + @dataclass class Config: """全局配置""" @@ -56,6 +71,7 @@ class Config: llm: LLMSettings = field(default_factory=LLMSettings) wechat: WeChatSettings = field(default_factory=WeChatSettings) rules: List[ReplyRule] = field(default_factory=list) + contacts: ContactsConfig = field(default_factory=ContactsConfig) knowledge_base_url: Optional[str] = None log_level: str = "INFO" @@ -122,6 +138,17 @@ class ConfigManager: ) config.rules.append(rule) + # 加载联系人配置 + if "contacts" in data: + contacts_data = data["contacts"] + if "priority" in contacts_data: + config.contacts.priority.enabled = contacts_data["priority"].get("enabled", True) + config.contacts.priority.users = contacts_data["priority"].get("users", []) + if "ignore" in contacts_data: + config.contacts.ignore.enabled = contacts_data["ignore"].get("enabled", True) + config.contacts.ignore.users = contacts_data["ignore"].get("users", []) + config.contacts.normal_auto_reply = contacts_data.get("normal", {}).get("auto_reply", False) + if "knowledge_base" in data: config.knowledge_base_url = data["knowledge_base"].get("url") diff --git a/src/core/__pycache__/engine.cpython-311.pyc b/src/core/__pycache__/engine.cpython-311.pyc index 1f64261..62456ea 100644 Binary files a/src/core/__pycache__/engine.cpython-311.pyc and b/src/core/__pycache__/engine.cpython-311.pyc differ diff --git a/src/core/contact_manager.py b/src/core/contact_manager.py new file mode 100644 index 0000000..8d5cc1b --- /dev/null +++ b/src/core/contact_manager.py @@ -0,0 +1,90 @@ +""" +联系人管理器 +管理联系人分类:priority / ignore / normal +""" + +import fnmatch +import logging +from typing import List, Optional + +from src.config.settings import ContactsConfig + +logger = logging.getLogger(__name__) + + +class ContactManager: + """联系人管理器""" + + def __init__(self, config: ContactsConfig): + self.config = config + + def is_priority(self, sender: str) -> bool: + """检查是否是重点用户""" + if not self.config.priority.enabled: + return False + return self._match_user(sender, self.config.priority.users) + + def is_ignored(self, sender: str) -> bool: + """检查是否在忽略列表""" + if not self.config.ignore.enabled: + return False + return self._match_user(sender, self.config.ignore.users) + + def should_auto_reply(self, sender: str) -> bool: + """判断是否应该自动回复""" + # 忽略列表中的不回复 + if self.is_ignored(sender): + return False + + # 重点用户自动回复 + if self.is_priority(sender): + return True + + # 普通用户根据配置决定 + return self.config.normal_auto_reply + + def _match_user(self, sender: str, patterns: List[str]) -> bool: + """匹配用户 + 支持: + - 精确匹配: "尾巴~" + - 通配符匹配: "张*" 匹配所有姓张的 + - 前缀匹配: "公众号:" 匹配所有公众号 + """ + if not sender or not patterns: + return False + + sender_lower = sender.lower() + + for pattern in patterns: + if not pattern: + continue + + # 精确匹配 + if sender == pattern: + return True + + # 忽略大小写匹配 + if sender_lower == pattern.lower(): + return True + + # 通配符匹配 (fnmatch) + if fnmatch.fnmatch(sender, pattern): + return True + + # 前缀匹配(如 "公众号:" 匹配 "公众号:xxx") + if pattern.endswith(":*") and sender.startswith(pattern[:-1]): + return True + + # 包含匹配 + if pattern in sender: + return True + + return False + + def get_contact_type(self, sender: str) -> str: + """获取联系人类型""" + if self.is_priority(sender): + return "priority" + if self.is_ignored(sender): + return "ignore" + return "normal" diff --git a/src/core/engine.py b/src/core/engine.py index f4538b1..c9c0653 100644 --- a/src/core/engine.py +++ b/src/core/engine.py @@ -11,6 +11,8 @@ from typing import List, Optional, Callable, Dict, Any from enum import Enum from queue import Queue +from src.core.contact_manager import ContactManager + logger = logging.getLogger(__name__) @@ -174,6 +176,7 @@ class WeChatAgent: self.llm = llm_client self.config = config self.processor = MessageProcessor(vlm_client, llm_client, config) + self.contact_manager = ContactManager(config.contacts) self._state = AgentState.IDLE self._thread: Optional[threading.Thread] = None @@ -269,16 +272,16 @@ class WeChatAgent: 流程: 1. 截图 - 2. VLM 分析(不依赖 has_new_message) - 3. 去重检查(20秒窗口) - 4. 回复决策(sender == "我" 则跳过) + 2. VLM 分析 + 3. 联系人分类(priority / ignore / normal) + 4. 去重检查 + 5. 发送回复 """ try: # 阶段1: 截图 screenshot_path = self.wechat.screenshot() # 阶段2: VLM 分析 - # 注意:不依赖 has_new_message(VLM 判断不可靠) chat_info = self.vlm.analyze_chat_screenshot(screenshot_path) chat_name = chat_info.get("current_chat", "") messages = chat_info.get("messages", []) @@ -288,50 +291,62 @@ class WeChatAgent: logger.debug("无消息,跳过") return - # 阶段3: 获取最新消息 + # 阶段3: 获取最新消息并分类 latest = messages[0] latest_sender = latest.get("sender", "").strip() latest_content = latest.get("content", "").strip()[:60] - # 调试日志 - logger.debug(f"[轮询] chat={chat_name}, sender={latest_sender}, content={latest_content[:30]}") - - # 阶段4: 回复决策 - # 规则:只有 sender 是"我"时才跳过(这是最可靠的判断) - skip_senders = {"我", "自己"} - if latest_sender in skip_senders: - logger.debug(f"己方消息,跳过: sender={latest_sender}") + # 跳过己方消息 + if latest_sender in ("我", "自己"): + logger.debug(f"己方消息,跳过") return - # 阶段5: 去重检查 + # 联系人分类决策 + contact_type = self.contact_manager.get_contact_type(latest_sender) + logger.debug(f"[消息] sender={latest_sender}, type={contact_type}, content={latest_content[:30]}") + + # 忽略列表:不处理 + if contact_type == "ignore": + logger.debug(f"忽略列表用户,不回复: {latest_sender}") + return + + # 普通用户:不自动回复(除非配置了 normal_auto_reply) + if contact_type == "normal": + should_reply = self.contact_manager.should_auto_reply(latest_sender) + if not should_reply: + logger.debug(f"普通用户,无需自动回复: {latest_sender}") + return + + # 阶段4: 去重检查 dedup_key = f"{latest_sender}|{latest_content}" if dedup_key in self._last_processed_time: elapsed = current_time - self._last_processed_time[dedup_key] if elapsed < 20: - logger.debug(f"消息已处理过({elapsed:.1f}s前),跳过: {latest_content[:30]}") + logger.debug(f"消息已处理过({elapsed:.1f}s前),跳过") return # 记录已处理 self._last_processed_time[dedup_key] = current_time - # 阶段6: 创建快照并发送回复 + # 阶段5: 创建快照并发送回复 snapshot = ChatSnapshot( timestamp=current_time, chat_name=chat_name, messages=messages, screenshot_path=screenshot_path, - has_new=True # 强制设为 True,因为我们相信有新消息 + has_new=True ) # 触发消息回调 self._emit("on_message", { "chat_name": chat_name, "latest_message": latest, - "all_messages": messages + "all_messages": messages, + "contact_type": contact_type }) # 生成并发送回复 - logger.info(f"检测到新消息: [{latest_sender}] {latest_content[:40]}") + logger.info(f"自动回复 [{contact_type}]: [{latest_sender}] {latest_content[:40]}") reply = self.processor.generate_reply(snapshot) if reply: