148 lines
4.5 KiB
Python
148 lines
4.5 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""微信自动回复接管脚本 v2(wechatauto-replica)
|
||
|
||
功能:监听微信新消息,当【真实好友】发来私聊消息时,自动回复。
|
||
规则:
|
||
- 只回复 wxid_ 开头的真实好友(公众号 gh_、群聊 @chatroom、
|
||
文件传输助手 filehelper、特殊号 @openim/@placeholder/brandsessionholder 一律不碰)
|
||
- 跳过自己发出去的消息(sender_id == 2)
|
||
- 同一好友 COOLDOWN_SEC 秒内只回一次(防刷屏)
|
||
- GUI 发送全局串行(多个好友同时来消息不会抢窗口)
|
||
|
||
用法(项目目录下,独立 venv):
|
||
.venv\\Scripts\\python.exe wechat_daemon.py
|
||
|
||
前置:微信 4.x 已登录、桌面未锁屏、管理员权限(提密钥)。
|
||
"""
|
||
import sys
|
||
import os
|
||
import time
|
||
import threading
|
||
|
||
try:
|
||
os.system("chcp 65001 >nul 2>&1")
|
||
except Exception:
|
||
pass
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||
except AttributeError:
|
||
pass
|
||
|
||
from wechatauto.db import WeChatDB, Listener
|
||
from wechatauto.guia import WeChatGUI
|
||
|
||
# ==================== 自动回复配置(改这里) ====================
|
||
AUTO_REPLY = True # 总开关
|
||
DEFAULT_REPLY = "【自动回复】已收到您的消息,稍后回复您。"
|
||
|
||
# 关键词规则:消息命中关键词时用对应话术回复;未命中则用 DEFAULT_REPLY
|
||
KEYWORD_RULES = {
|
||
# "你好": "你好呀,请问有什么可以帮您?",
|
||
# "价格": "具体价格您可以看下我发给您的报价单。",
|
||
}
|
||
|
||
# 只回复这些前缀的会话(真实好友)。留空 = 不限制前缀
|
||
REPLY_ONLY_PREFIX = ("wxid_",)
|
||
|
||
# 防抖:同一会话内多少秒内不重复自动回复
|
||
COOLDOWN_SEC = 30
|
||
|
||
# 黑名单会话(精确匹配 username,绝不自动回复)
|
||
BLOCKLIST = {"filehelper"}
|
||
# ==============================================================
|
||
|
||
_gui = None
|
||
_gui_lock = threading.Lock()
|
||
_last_reply = {} # username -> 上次回复时间戳
|
||
_send_lock = threading.Lock()
|
||
|
||
|
||
def get_gui():
|
||
global _gui
|
||
with _gui_lock:
|
||
if _gui is None:
|
||
_gui = WeChatGUI()
|
||
return _gui
|
||
|
||
|
||
def should_reply(username: str) -> bool:
|
||
"""是否对某会话启用自动回复。"""
|
||
if not AUTO_REPLY:
|
||
return False
|
||
if username in BLOCKLIST:
|
||
return False
|
||
if username.endswith("@chatroom"):
|
||
return False
|
||
if not REPLY_ONLY_PREFIX:
|
||
return True
|
||
return username.startswith(REPLY_ONLY_PREFIX)
|
||
|
||
|
||
def pick_reply(content: str) -> str:
|
||
for kw, reply in KEYWORD_RULES.items():
|
||
if kw in content:
|
||
return reply
|
||
return DEFAULT_REPLY
|
||
|
||
|
||
def on_message(msg: dict, lst: Listener):
|
||
username = msg.get("username", "")
|
||
sender_id = msg.get("sender_id", "")
|
||
content = str(msg.get("content", ""))
|
||
|
||
# 跳过自己发出去的消息(避免回复自己的回复 → 死循环)
|
||
if str(sender_id) == "2":
|
||
return
|
||
|
||
if not should_reply(username):
|
||
return
|
||
|
||
# 防抖
|
||
now = time.time()
|
||
last = _last_reply.get(username, 0)
|
||
if now - last < COOLDOWN_SEC:
|
||
return
|
||
_last_reply[username] = now
|
||
|
||
reply = pick_reply(content)
|
||
nick = _last_reply.get("_db") and _last_reply["_db"].get_nickname(username) or username
|
||
# 全局串行发送,避免多个会话并发抢同一微信窗口
|
||
with _send_lock:
|
||
try:
|
||
r = get_gui().send_msg(reply, who=username, verify=True)
|
||
print(f"[自动回复] {username} ({nick}) <- {reply} | ok={r.is_success} {r['message']}",
|
||
flush=True)
|
||
except Exception as e: # noqa: BLE001
|
||
print(f"[自动回复失败] {username}: {e!r}", flush=True)
|
||
|
||
|
||
def main():
|
||
db = WeChatDB()
|
||
info = db.get_self_info()
|
||
nick = info.get("nick_name") or info.get("username")
|
||
_last_reply["_db"] = db
|
||
print(f"已接管微信: {nick} ({info.get('username')})", flush=True)
|
||
print(f"自动回复: {'开' if AUTO_REPLY else '关'} | 默认话术: {DEFAULT_REPLY} | "
|
||
f"只回前缀: {REPLY_ONLY_PREFIX} | 防抖 {COOLDOWN_SEC}s", flush=True)
|
||
|
||
lst = Listener(db, interval=1.0)
|
||
# add_all:监听所有已有会话 + 自动发现新会话
|
||
lst.add_all(on_message)
|
||
print("自动回复监听已启动(真实好友发消息即自动回复,公众号/群聊不碰),Ctrl+C 退出...",
|
||
flush=True)
|
||
|
||
lst.start()
|
||
try:
|
||
while True:
|
||
time.sleep(1)
|
||
except KeyboardInterrupt:
|
||
pass
|
||
finally:
|
||
lst.stop()
|
||
print("已停止监听。", flush=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|