""" WeChat Agent v2 - wxhelper DLL + Hermes API (:8642) """ import os, json, time, threading, requests, re, socketserver, subprocess, urllib.request, urllib.error os.environ["no_proxy"] = "*" os.environ["NO_PROXY"] = "*" from http.server import HTTPServer, BaseHTTPRequestHandler BOT_WXID = "wxid_7onnerpx2s2l22" BLOCK_WXIDS = {"fmessage", "weixin", "wechat"} # 系统账号/微信团队,不回复 WX_API = "http://127.0.0.1:19088" LOG_FILE = r"C:\Users\hmo\Desktop\wechat_agent.log" TCP_PORT = 19099 last_msg_time = time.time() nickname_cache = {} HERMES_API = "http://192.168.0.103:8642/v1/chat/completions" HERMES_KEY = "hermes123" SENSENOVA_KEY = "sk-aRNj3UwKSLPsDfh15QNTPwbHxahblfaO" SENSENOVA_URL = "https://token.sensenova.cn/v1" INJECTOR = r"C:\Users\hmo\Desktop\wxhelper_v11\wxhelper-3.9.5.81-v11\tool\injector\ConsoleApplication.exe" WXHELPER_DLL = r"C:\Users\hmo\Desktop\wxhelper_391019.dll" def log(m): with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"{time.strftime('%H:%M:%S')} {m}\n") def wxpost(path, data=None, timeout=10): try: body = json.dumps(data or {}).encode() r = urllib.request.urlopen(urllib.request.Request(WX_API + path, data=body, headers={"Content-Type": "application/json"}), timeout=timeout) return json.loads(r.read().decode()) except urllib.error.HTTPError as e: return json.loads(e.read().decode()) if e.code else {"code": -1} except Exception as e: log(f"WX ERR: {e}") return {"code": -1} def send_wx(wxid, msg): # Strip weixin:// URLs that WeChat interprets as commands import re as _re2 msg = _re2.sub(r'weixin://[^\s]+', '[链接已过滤]', msg) r = wxpost("/api/sendTextMsg", {"wxid": wxid, "msg": msg}) log(f"SEND {wxid}: {r.get('msg','')}") def get_nickname(wxid): if wxid in nickname_cache: return nickname_cache[wxid] r = wxpost("/api/getContactList", timeout=10) for c in (r.get("data") or []): if c.get("wxid") == wxid: nick = c.get("nickname") or c.get("customAccount") or wxid nickname_cache[wxid] = nick return nick nickname_cache[wxid] = wxid return wxid def call_hermes(wxid, content): nickname = get_nickname(wxid) headers = {"Authorization": f"Bearer {HERMES_KEY}", "X-Hermes-Session-Id": "sisyphus", "Content-Type": "application/json"} sys_prompt = "回复简短。" body = {"model": "hermes-agent", "messages": [{"role": "system", "content": sys_prompt}, {"role": "user", "content": content}]} try: r = requests.post(HERMES_API, json=body, headers=headers, timeout=180, proxies={"http": None, "https": None}) if r.status_code == 200: return r.json()["choices"][0]["message"]["content"] except Exception as e: log(f"API ERR: {e}") return None # ── Inject wxhelper DLL ── def inject_wxhelper(): try: r = wxpost("/api/checkLogin", timeout=5) if r.get("code") == 1: log("wxhelper already injected") return True except: pass try: result = subprocess.run([INJECTOR, "-i", "WeChat.exe", "-p", WXHELPER_DLL], capture_output=True, text=True, timeout=30) log(f"Inject: {result.stdout.strip()[:50]}") time.sleep(3) r = wxpost("/api/checkLogin", timeout=5) if r.get("code") == 1: log("wxhelper injected OK") return True log(f"Inject check: {r}") return False except Exception as e: log(f"Inject FAIL: {e}") return False # ── TCP Message Receiver ── class MsgHandler(socketserver.BaseRequestHandler): def handle(self): try: data = b"" while True: c = self.request.recv(4096) data += c if not c or c[-1] == 10: break if data.strip(): threading.Thread(target=process_msg, args=(data,), daemon=True).start() self.request.sendall(b"200 OK\n") except: pass finally: self.request.close() def process_msg(raw_data): global last_msg_time last_msg_time = time.time() try: d = json.loads(raw_data) log(f"RAW: fromUser={d.get('fromUser','')} type={d.get('type','')} self={d.get('isSelf',d.get('self',0))}") fu = d.get("fromUser", "") or d.get("fromuser", "") or d.get("sender", "") ct = d.get("content", "") or d.get("msg", "") or d.get("text", "") msg_type = d.get("type", 1) is_self = d.get("isSelf", 0) or d.get("self", 0) if not fu or not ct or fu == BOT_WXID or fu in BLOCK_WXIDS or fu.startswith("gh_") or is_self: log(f"SKIP: fu={fu} self={is_self}") return # Route by message type if msg_type == 34: # Voice log(f"<- {fu}: [voice]") reply = call_hermes(fu, "[voice message]") if reply: send_wx(fu, reply) return if msg_type == 3: # Image - wxhelper sends image as separate event return # Text - prepend sender wxid+name so Hermes knows who's talking sender_name = get_nickname(fu) msg_with_sender = f"[{fu}|{sender_name}] {ct}" log(f"<- {fu} ({sender_name}): {ct[:50]}") reply = call_hermes(fu, msg_with_sender) if reply: log(f"-> {fu}: {reply[:50]}") process_tags(reply, fu) else: log(f"-> {fu}: no reply") except Exception as e: log(f"MSG ERR: {e}") import traceback log(f"TRACE: {traceback.format_exc()[:200]}") def process_tags(reply, fu): if not reply: return clean = reply # [FILE] for tag, pattern, repl in [ ("FILE", r'\[FILE\](.*?)\[/FILE\]', lambda m: download_and_send_file(m, fu)), ("IMG", r'\[IMG\](.*?)\[/IMG\]', lambda m: handle_img(m, fu)), ("EMOJI", r'\[EMOJI\](.*?)\[/EMOJI\]', lambda m: download_emoji(m, fu)), ]: match = re.search(pattern, clean) if match: clean = re.sub(r'\s*' + pattern.replace('(.*?)', '.*?') + r'\s*', '', clean).strip() try: match = re.search(pattern, reply) # re-match against original if match: threading.Thread(target=repl, args=(match,), daemon=True).start() except: pass # [CONTACT:wxid] cm = re.search(r'\[CONTACT:(\w+)\]', clean) if cm: clean = re.sub(r'\s*\[CONTACT:\w+\]\s*', '', clean).strip() r = wxpost("/api/getContactProfile", {"wxid": cm.group(1)}) cd = r.get("data", {}) send_wx(fu, f"昵称: {cd.get('nickname','?')} 备注: {cd.get('remark','')}") # [ROOM_MEMBERS:roomid] rm = re.search(r'\[ROOM_MEMBERS:(\S+)\]', clean) if rm: clean = re.sub(r'\s*\[ROOM_MEMBERS:\S+\]\s*', '', clean).strip() r = wxpost("/api/getMemberFromChatRoom", {"chatRoomId": rm.group(1)}) members = (r.get("data") or {}).get("members", "") mlist = [m for m in members.split("\u0007") if m] send_wx(fu, f"群成员 ({len(mlist)}): {','.join(mlist[:20])}") # [PAT:roomid:wxid] pm = re.search(r'\[PAT:(\S+):(\S+)\]', clean) if pm: clean = re.sub(r'\s*\[PAT:\S+:\S+\]\s*', '', clean).strip() wxpost("/api/sendPatMsg", {"receiver": pm.group(1), "wxid": pm.group(2)}) if clean.strip(): send_wx(fu, clean.strip()) def download_and_send_file(m, fu): url = m.group(1).strip() ir = requests.get(url, timeout=60, proxies={"http": None, "https": None}) if ir.status_code == 200: tmp = os.path.join(r"C:\Users\hmo\Desktop", f"send_file_{int(time.time())}.dat") with open(tmp, "wb") as f: f.write(ir.content) wxpost("/api/sendFileMsg", {"wxid": fu, "filePath": tmp}) os.remove(tmp) def handle_img(m, fu): cmd = m.group(1).strip() if cmd.startswith("generate:") or cmd.startswith("draw:"): parts = cmd.split(":", 1)[1].strip() ratio = "1:1" if "|" in parts: ratio = parts.split("|")[1].strip() prompt = parts.split("|")[0].strip() else: prompt = parts size_map = {"1:1":"2048x2048","16:9":"2752x1536","9:16":"1536x2752","3:2":"2496x1664","2:3":"1664x2496","3:4":"1760x2368","4:3":"2368x1760"} size = size_map.get(ratio, "2048x2048") log(f"GEN SenseNova: {prompt[:30]} [{ratio}]") r = requests.post(SENSENOVA_URL + "/images/generations", json={"model": "sensenova-u1-fast", "prompt": prompt, "size": size, "response_format": "url"}, headers={"Authorization": f"Bearer {SENSENOVA_KEY}", "Content-Type": "application/json"}, timeout=180) if r.status_code == 200: img_url = r.json()["data"][0]["url"] ir = requests.get(img_url, timeout=60) if ir.status_code == 200: tmp = os.path.join(r"C:\Users\hmo\Desktop", f"gen_img_{int(time.time())}.png") with open(tmp, "wb") as f: f.write(ir.content) wxpost("/api/sendImagesMsg", {"wxid": fu, "imagePath": tmp}) os.remove(tmp) else: ir = requests.get(cmd, timeout=30, proxies={"http": None, "https": None}) if ir.status_code == 200: ext = ".jpg" if "png" in ir.headers.get("content-type", ""): ext = ".png" tmp = os.path.join(r"C:\Users\hmo\Desktop", f"send_img_{int(time.time())}{ext}") with open(tmp, "wb") as f: f.write(ir.content) wxpost("/api/sendImagesMsg", {"wxid": fu, "imagePath": tmp}) os.remove(tmp) def download_emoji(m, fu): url = m.group(1).strip() ir = requests.get(url, timeout=30, proxies={"http": None, "https": None}) if ir.status_code == 200: tmp = os.path.join(r"C:\Users\hmo\Desktop", f"emoji_{int(time.time())}.png") with open(tmp, "wb") as f: f.write(ir.content) wxpost("/api/sendCustomEmotion", {"wxid": fu, "filePath": tmp}) os.remove(tmp) # ── Watchdog ── def watchdog(): global last_msg_time while True: idle = time.time() - last_msg_time if idle > 120: try: r = wxpost("/api/checkLogin", timeout=5) if r.get("code") == 1: wxpost("/api/hookSyncMsg", {"ip": "127.0.0.1", "port": TCP_PORT, "enableHttp": 0}) log(f"WATCHDOG: refreshed ({int(idle)}s)") else: log("WATCHDOG: re-injecting...") inject_wxhelper() except: pass last_msg_time = time.time() time.sleep(30) # ── Start ── print("[Agent] starting...", flush=True) log("=== Agent v2 (wxhelper) ===") # Inject wxhelper inject_wxhelper() # Check login r = wxpost("/api/checkLogin") if r.get("code") == 1: log(f"Logged in: OK") else: log(f"Login check: {r}") log("Will retry via watchdog") # Start watchdog threading.Thread(target=watchdog, daemon=True).start() # Start TCP server for message receiving tcp_server = socketserver.ThreadingTCPServer(("127.0.0.1", TCP_PORT), MsgHandler) threading.Thread(target=tcp_server.serve_forever, daemon=True).start() log(f"TCP server on :{TCP_PORT}") # Hook sync messages (tell DLL to send events to our TCP server) r = wxpost("/api/hookSyncMsg", {"port": TCP_PORT, "ip": "127.0.0.1", "enableHttp": 0}) log(f"hookSyncMsg: {r}") # Reply server for hermes-msg class RH(BaseHTTPRequestHandler): def do_POST(self): global last_msg_time last_msg_time = time.time() body = self.rfile.read(int(self.headers.get("Content-Length", 0))) try: d = json.loads(body) if self.path == "/hermes-msg": msg = d.get("message", "") or d.get("content", "") or str(d)[:200] log("<<< HERMES: " + msg[:100]) with open(r"C:\Users\hmo\Desktop\hermes_inbox.txt", "a", encoding="utf-8") as f: f.write(f"{time.strftime('%H:%M:%S')} {msg}\n") self.send_response(200); self.end_headers(); return to = d.get("to", "") or d.get("wxid", "") msg = d.get("message", "") or d.get("content", "") if to and msg: log(f"REPLY {to}: {msg[:50]}") send_wx(to, msg) except Exception as e: log(f"RH ERR: {e}") self.send_response(200); self.end_headers() def do_GET(self): self.send_response(200); self.end_headers(); self.wfile.write(b'{"ok":true}') def log_message(self, *a): pass threading.Thread(target=lambda: HTTPServer(("0.0.0.0", 5801), RH).serve_forever(), daemon=True).start() log("HTTP :5801") # Notify user send_wx("filehelper", "[Agent v2] wxhelper online") log("Ready") print(f"[Agent v2] wxhelper :19088 | Hermes :8642") try: while True: time.sleep(1) except KeyboardInterrupt: log("Bye")