全面升级:WeChat 3.9.5.81 + 全尺寸图片 OCR + 启动脚本

This commit is contained in:
hmo
2026-05-21 01:31:31 +08:00
parent 4296af615f
commit 1593105a4a
9 changed files with 634 additions and 54 deletions
+39
View File
@@ -0,0 +1,39 @@
@echo off
title WeChat Agent
set PROJECT_DIR=D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway
set TOOLS_DIR=%PROJECT_DIR%\tools
set PYTHONW=C:\Users\hmo\AppData\Local\Programs\Python\Python310\pythonw.exe
set INJECTOR=%TOOLS_DIR%\Injector_x64.exe
set DLL=%TOOLS_DIR%\wxhelper_official_39581.dll
set LOG=%PROJECT_DIR%\logs\startup.log
echo [1/4] Waiting for WeChat...
:wait_wechat
tasklist /fi "imagename eq WeChat.exe" 2>nul | find /i "WeChat.exe" >nul
if errorlevel 1 (
timeout /t 2 /nobreak >nul
goto wait_wechat
)
echo [2/4] WeChat started, checking wxhelper...
curl -s -m 3 -X POST http://127.0.0.1:19088/api/checkLogin -H "Content-Type: application/json" -d "{}" 2>nul | find "code" >nul
if not errorlevel 1 (
echo [3/4] wxhelper OK, skipping inject
goto start_agent
)
echo [3/4] Injecting wxhelper...
%INJECTOR% -n WeChat.exe -i "%DLL%" >> "%LOG%" 2>&1
echo [3/4] Waiting for wxhelper HTTP...
:wait_wxhelper
timeout /t 2 /nobreak >nul
curl -s -m 3 -X POST http://127.0.0.1:19088/api/checkLogin -H "Content-Type: application/json" -d "{}" 2>nul | find "code" >nul
if errorlevel 1 goto wait_wxhelper
:start_agent
echo [4/4] Clearing cache and starting agent...
if exist "%PROJECT_DIR%\scripts\__pycache__" rmdir /s /q "%PROJECT_DIR%\scripts\__pycache__"
start "" "%PYTHONW%" "%PROJECT_DIR%\scripts\wechat_agent.py"
echo Done.
+206 -27
View File
@@ -1,14 +1,14 @@
"""
"""
WeChat Agent v2 - wxhelper DLL + Hermes API (:8642)
"""
import os, json, time, threading, requests, re, socketserver, subprocess, urllib.request, urllib.error
import os, json, time, threading, requests, re, socketserver, subprocess, urllib.request, urllib.error, base64
os.environ["no_proxy"] = "*"
os.environ["NO_PROXY"] = "*"
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
BOT_WXID = "wxid_7onnerpx2s2l22"
BLOCK_WXIDS = {"fmessage", "weixin", "wechat"} # 系统账号/微信团队,不回复
BLOCK_WXIDS = {"fmessage", "weixin", "wechat"} # ϵͳ˺Ŷӣظ
WX_API = "http://127.0.0.1:19088"
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
@@ -26,8 +26,8 @@ HERMES_KEY = "hermes123"
SENSENOVA_KEY = "sk-aRNj3UwKSLPsDfh15QNTPwbHxahblfaO"
SENSENOVA_URL = "https://token.sensenova.cn/v1"
INJECTOR = r"D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway\tools\ConsoleApplication.exe"
WXHELPER_DLL = r"D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway\tools\wxhelper_391019.dll"
INJECTOR = r"D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway\tools\Injector_x64.exe"
WXHELPER_DLL = r"D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway\tools\wxhelper_official_39581.dll"
def log(m):
with open(LOG_FILE, "a", encoding="utf-8") as f:
@@ -44,7 +44,7 @@ def wxpost(path, data=None, timeout=10):
log(f"WX ERR: {e}")
return {"code": -1}
# ── History Query (via MSG table in MSG*.db databases) ──
# History Query (via MSG table in MSG*.db databases)
def get_db_handle():
"""Get handle for database containing MSG table. Cached after first call."""
global db_handle_cache
@@ -78,7 +78,7 @@ def get_db_handle():
return None
# Message type labels
MSG_TYPES = {1: "文字", 3: "图片", 34: "语音", 43: "视频", 47: "表情", 49: "链接", 10000: "系统", 10002: "红包"}
MSG_TYPES = {1: "", 3: "ͼƬ", 34: "", 43: "Ƶ", 47: "", 49: "", 10000: "ϵͳ", 10002: ""}
def query_history(wxid, limit=10):
"""Query historical text messages with a contact from MSG table."""
@@ -110,7 +110,7 @@ def format_history(wxid, rows):
"""Format MSG rows into readable chat history text."""
sender_name = get_nickname(wxid)
bot_name = get_nickname(BOT_WXID)
lines = [f"📜 最近与 {sender_name} 的聊天记录 ({len(rows)}):"]
lines = [f"?? {sender_name} ¼ ({len(rows)}):"]
for row in rows:
ts = int(row.get("CreateTime", 0))
time_str = time.strftime("%m/%d %H:%M", time.localtime(ts)) if ts else "?"
@@ -121,7 +121,7 @@ def format_history(wxid, rows):
who = bot_name if is_sender else sender_name
# Format content
if msg_type == 49:
content = f"[链接] {content[:60]}"
content = f"[] {content[:60]}"
else:
content = content[:200]
lines.append(f"[{time_str}] {who}: {content}")
@@ -133,10 +133,10 @@ def handle_history(wxid, count):
rows = query_history(wxid, count)
if rows:
return format_history(wxid, rows)
return f"暂无与 {get_nickname(wxid)} 的聊天记录"
return f" {get_nickname(wxid)} ¼"
except Exception as e:
log(f"History ERR: {e}")
return "查询历史记录失败"
return "ѯʷ¼ʧ"
def handle_history_json(wxid, count):
"""Query history and return JSON-serializable dict for HTTP API."""
@@ -176,7 +176,7 @@ def handle_history_json(wxid, count):
def send_wx(wxid, msg):
# Strip weixin:// URLs that WeChat interprets as commands
import re as _re2
msg = _re2.sub(r'weixin://[^\s]+', '[链接已过滤]', msg)
msg = _re2.sub(r'weixin://[^\s]+', '[ѹ]', msg)
r = wxpost("/api/sendTextMsg", {"wxid": wxid, "msg": msg})
log(f"SEND {wxid}: {r.get('msg','')}")
@@ -195,7 +195,7 @@ def get_nickname(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 = "回复简短。"
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})
@@ -208,7 +208,7 @@ def call_hermes(wxid, content):
def inject_to_hermes_session(text):
"""Inject chat history / context directly into Hermes's sisyphus session for memory repair."""
headers = {"Authorization": f"Bearer {HERMES_KEY}", "X-Hermes-Session-Id": "sisyphus", "Content-Type": "application/json"}
sys_prompt = "📥 MEMORY INJECTION: Below is past chat history. Absorb this into your context for memory repair. Do NOT reply to this just acknowledge with 'Memory synced.'"
sys_prompt = "?? MEMORY INJECTION: Below is past chat history. Absorb this into your context for memory repair. Do NOT reply to this just acknowledge with 'Memory synced.'"
body = {"model": "hermes-agent", "messages": [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": text}
@@ -224,7 +224,7 @@ def inject_to_hermes_session(text):
log(f"Inject history ERR: {e}")
return False
# ── Inject wxhelper DLL ──
# Inject wxhelper DLL
def inject_wxhelper():
try:
r = wxpost("/api/checkLogin", timeout=5)
@@ -233,9 +233,37 @@ def inject_wxhelper():
return True
except:
pass
# Also check if port 19088 is just listening (wxhelper HTTP server alive)
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]}")
import socket as _sock
s = _sock.create_connection(("127.0.0.1", 19088), timeout=2)
s.close()
r = wxpost("/api/checkLogin", timeout=5)
if r.get("code") == 1:
log("wxhelper HTTP server alive, login OK")
return True
except:
pass
# Wait a moment in case server is still starting
time.sleep(3)
try:
r = wxpost("/api/checkLogin", timeout=5)
if r.get("code") == 1:
log("wxhelper responding after wait")
return True
except:
pass
try:
# Injector_x64.exe: -n process_name -i dll_path
result = subprocess.run([INJECTOR, "-n", "WeChat.exe", "-i", WXHELPER_DLL], capture_output=True, text=True, timeout=30)
output = (result.stdout + result.stderr).strip()
log(f"Inject: {output[:100]}")
# Check if injection succeeded by looking for "success" in output
if "success" not in output.lower():
log(f"Inject MAY HAVE FAILED (no 'success' in output), retrying...")
time.sleep(2)
result2 = subprocess.run([INJECTOR, "-n", "WeChat.exe", "-i", WXHELPER_DLL], capture_output=True, text=True, timeout=30)
log(f"Inject retry: {(result2.stdout+result2.stderr).strip()[:100]}")
time.sleep(3)
r = wxpost("/api/checkLogin", timeout=5)
if r.get("code") == 1:
@@ -247,7 +275,7 @@ def inject_wxhelper():
log(f"Inject FAIL: {e}")
return False
# ── TCP Message Receiver ──
# TCP Message Receiver
class MsgHandler(socketserver.BaseRequestHandler):
def handle(self):
try:
@@ -265,6 +293,128 @@ class MsgHandler(socketserver.BaseRequestHandler):
finally:
self.request.close()
# Image OCR
WX_FILES_BASE = os.path.join(os.path.expanduser("~"), "Documents", "WeChat Files")
BOT_WX_DIR = os.path.join(WX_FILES_BASE, BOT_WXID, "wxhelper")
def ocr_image(base64_data):
"""OCR from in-memory base64 image data. Returns text or None."""
try:
headers = {"Authorization": "Bearer b0359bed-09f2-49e2-a53c-32ba057412e3", "Content-Type": "application/json"}
payload = {
"model": "doubao-seed-code",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "请识别这张图片中的所有中文和英文字符,保持原文输出,包括数字、表格、百分比的完整结构。严格逐行逐列输出所有数据,不要省略、不要总结。"},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_data}"}}
]
}]
}
r = requests.post(
"https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions",
json=payload, headers=headers, timeout=60,
proxies={"http": None, "https": None}
)
if r.status_code == 200:
text = r.json()["choices"][0]["message"]["content"].strip()
log(f"OCR OK ({len(text)} chars)")
return text
log(f"OCR HTTP {r.status_code}: {r.text[:200]}")
except Exception as e:
log(f"OCR ERR: {e}")
return None
def ocr_image_file(image_path):
"""OCR an image file on disk. Returns text or None."""
try:
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
return ocr_image(b64)
except Exception as e:
log(f"ocr_image_file ERR: {e}")
return None
# Full Image Download & Decode (wxhelper 3.9.5.81+)
def download_full_image(msg_id):
"""Download full image from CDN via downloadAttach. Returns encrypted .dat path or None.
Retries both the API call (wxhelper may return -2 transiently)
and file existence (async CDN download takes time).
"""
try:
dat_path = os.path.join(BOT_WX_DIR, "image", f"{msg_id}.dat")
# Phase 1: Retry API call (wxhelper may return -2 if msg not ready)
for api_attempt in range(10):
r = wxpost("/api/downloadAttach", {"msgId": int(msg_id)}, timeout=30)
code = r.get("code", -1)
if code >= 0:
break
log(f"downloadAttach attempt {api_attempt+1}: code={code} {r.get('msg','')}")
time.sleep(1)
else:
log(f"downloadAttach FAILED after 10 attempts, last code={code}")
return None
# Phase 2: Wait for async CDN download
log(f"downloadAttach queued, waiting for file...")
for wait_attempt in range(20):
if os.path.exists(dat_path):
log(f"Download OK: {dat_path} ({os.path.getsize(dat_path)} bytes)")
return dat_path
time.sleep(1)
log(f"downloadAttach: .dat not found after 20s for msgId={msg_id}")
except Exception as e:
log(f"downloadAttach ERR: {e}")
return None
def decode_image_file(dat_path):
"""Decrypt encrypted .dat to viewable image. Returns decoded path or None.
Some .dat files are already valid PNG/JPEG images (not encrypted).
Falls back to checking if .dat itself is a valid image.
"""
try:
before_files = set(os.listdir(TEMP_DIR))
r = wxpost("/api/decodeImage", {"filePath": dat_path, "storeDir": TEMP_DIR}, timeout=30)
if r.get("code", -1) > 0:
base = os.path.splitext(os.path.basename(dat_path))[0]
for ext in ['.jpg', '.jpeg', '.png', '.bmp']:
cand = os.path.join(TEMP_DIR, base + ext)
if os.path.exists(cand):
log(f"Decoded: {cand}")
return cand
for f in os.listdir(TEMP_DIR):
if f in before_files: continue
if f.lower().endswith(('.jpg', '.jpeg', '.png')):
cand = os.path.join(TEMP_DIR, f)
log(f"Decoded (new): {cand}")
return cand
log("decodeImage OK but no new image file found")
# Fallback: .dat file may already be a valid image (not encrypted)
with open(dat_path, "rb") as f:
header = f.read(4)
ext = None
if header[:2] == b'\xff\xd8': # JPEG
ext = '.jpg'
elif header[:4] == b'\x89PNG': # PNG
ext = '.png'
elif header[:4] == b'GIF8': # GIF
ext = '.gif'
elif header[:2] == b'BM': # BMP
ext = '.bmp'
if ext:
out_path = os.path.join(TEMP_DIR, os.path.splitext(os.path.basename(dat_path))[0] + ext)
import shutil
shutil.copy(dat_path, out_path)
log(f".dat is already {ext}, copied to {out_path}")
return out_path
log(f"decodeImage FAIL: code={r.get('code')} {r.get('msg','')}")
except Exception as e:
log(f"decodeImage ERR: {e}")
return None
def process_msg(raw_data):
global last_msg_time
last_msg_time = time.time()
@@ -278,24 +428,52 @@ def process_msg(raw_data):
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
# 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)
if reply and reply.strip():
send_wx(fu, reply.strip())
return
if msg_type == 3: # Image - wxhelper sends image as separate event
if msg_type == 3: # Image
msg_id = d.get("msgId", 0) or d.get("svrid", 0)
log(f"IMAGE: msgId={msg_id} b64_len={len(d.get('base64Img',''))}")
ocr_text = None
# Full-image OCR via wxhelper 3.9.5.81 APIs
if msg_id:
dat_path = download_full_image(msg_id)
if dat_path:
decoded = decode_image_file(dat_path)
if decoded:
log(f"Full image OCR on {decoded}")
ocr_text = ocr_image_file(decoded)
if ocr_text:
log(f"OCR result ({len(ocr_text)} chars): {ocr_text[:200]}")
reply = call_hermes(fu, f"[老莫发送了一张图片,OCR识别结果如下]\n{ocr_text}")
elif msg_id:
# Had msgId but full-image OCR failed - report error, don't use thumbnail
log("Full-image OCR failed, skipping thumbnail (useless at 84x210)")
reply = call_hermes(fu, "[老莫发送了一张图片,但全尺寸图片下载或OCR识别失败,无法读取内容]")
else:
# No msgId at all - rare, just report failure
log("No msgId available, cannot download full image")
reply = call_hermes(fu, "[老莫发送了一张图片,但无法获取图片ID,无法识别]")
if reply and reply.strip():
log(f"-> {fu}: {reply[:50]}")
process_tags(reply, fu)
else:
log(f"-> {fu}: skip (blank image response)")
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:
if reply and reply.strip():
log(f"-> {fu}: {reply[:50]}")
process_tags(reply, fu)
else:
log(f"-> {fu}: no reply")
log(f"-> {fu}: no reply (blank/empty)")
except Exception as e:
log(f"MSG ERR: {e}")
import traceback
@@ -325,7 +503,7 @@ def process_tags(reply, fu):
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','')}")
send_wx(fu, f"dz: {cd.get('nickname','?')} ע: {cd.get('remark','')}")
# [ROOM_MEMBERS:roomid]
rm = re.search(r'\[ROOM_MEMBERS:(\S+)\]', clean)
if rm:
@@ -333,7 +511,7 @@ def process_tags(reply, fu):
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])}")
send_wx(fu, f"ȺԱ ({len(mlist)}): {','.join(mlist[:20])}")
# [HISTORY:wxid:count] - query chat history from MSG table
hm = re.search(r'\[HISTORY:(\S+?):(\d+)\]', clean)
if hm:
@@ -400,7 +578,7 @@ def download_emoji(m, fu):
wxpost("/api/sendCustomEmotion", {"wxid": fu, "filePath": tmp})
os.remove(tmp)
# ── Watchdog ──
# Watchdog
def watchdog():
global last_msg_time
while True:
@@ -419,7 +597,7 @@ def watchdog():
last_msg_time = time.time()
time.sleep(30)
# ── Start ──
# Start
print("[Agent] starting...", flush=True)
log("=== Agent v2 (wxhelper) ===")
@@ -519,3 +697,4 @@ try:
time.sleep(1)
except KeyboardInterrupt:
log("Bye")