v2: project cleanup, Desktop paths fixed, README updated, serve daemon added, mohe-xxm protocol documented

This commit is contained in:
hmo
2026-05-20 03:51:59 +08:00
parent 9e4c50a8a7
commit 3425ded733
10 changed files with 1090 additions and 163 deletions
-75
View File
@@ -1,75 +0,0 @@
"""
[DEPRECATED] 早期架构组件,已由 wechat_agent.py 直调 Hermes API 替代。
不再使用,仅作为架构参考保留。
"""
import pymem, pymem.process, requests, time, json, threading
from http.server import HTTPServer, BaseHTTPRequestHandler
DLL = r"C:\Users\hmo\AppData\Local\Programs\Python\Python310\Lib\site-packages\wxhook\tools\wxhook.dll"
WX = "http://127.0.0.1:19088"
HERMES = "http://192.168.0.103:5800/callback"
LOG = r"C:\Users\hmo\Desktop\bridge.log"
BOT = "wxid_7onnerpx2s2l22"
PORT = 5801
def log(m):
with open(LOG, "a", encoding="utf-8") as f:
f.write(f"{time.strftime('%H:%M:%S')} {m}\n")
# Inject DLL
try:
requests.post(f"{WX}/api/checkLogin", json={}, timeout=3)
log("DLL OK")
except:
pm = pymem.Pymem("WeChat.exe")
pymem.process.inject_dll(pm.process_handle, DLL.encode())
time.sleep(2)
log("DLL injected")
log(f"login: {requests.post(f'{WX}/api/checkLogin', json={}, timeout=5).json()}")
# Periodic webhook refresh (every 30 seconds)
def webhook_keepalive():
while True:
try:
requests.post(f"{WX}/api/hookSyncMsg", json={
"port": PORT, "ip": "0.0.0.0", "enableHttp": 1,
"url": f"http://127.0.0.1:{PORT}", "timeout": 300
}, timeout=5)
except:
pass
time.sleep(30)
threading.Thread(target=webhook_keepalive, daemon=True).start()
log("keepalive started (30s)")
# Test message
requests.post(f"{WX}/api/sendTextMsg",
json={"wxid": "filehelper", "msg": "[Bridge] online"}, timeout=5)
class H(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
try:
d = json.loads(body)
fu = d.get("fromUser", "")
ct = d.get("content", "")
if fu and ct and fu != BOT:
log(f"MSG {fu}: {ct[:60]}")
threading.Thread(target=lambda: requests.post(HERMES, json=[{"id": int(time.time()*1000), "type": 1, "content": ct, "sender": fu, "roomid": "", "ts": time.time()}], timeout=30), daemon=True).start()
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:
r = requests.post(f"{WX}/api/sendTextMsg", json={"wxid": to, "msg": msg}, timeout=5)
log(f"SEND {to}: {r.get('msg','')}")
self.send_response(200); self.end_headers(); return
except Exception as e:
log(f"ERR: {str(e)[:80]}")
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
log(f"ready on :{PORT}")
HTTPServer(("0.0.0.0", PORT), H).serve_forever()
+118
View File
@@ -0,0 +1,118 @@
# Mohe & XiaoXiaoMo Chat Viewer
# Lists [mohe]/[xxm] messages from an OpenCode session.
#
# Usage:
# .\moho_chat.ps1 <session_id> [minutes]
#
# Examples:
# .\moho_chat.ps1 ses_1d95d15c4ffehQaZ6hrbIbak5k
# .\moho_chat.ps1 ses_1d95d15c4ffehQaZ6hrbIbak5k 30
param(
[Parameter(Mandatory=$true)][string]$SessionId,
[int]$Minutes = 0
)
$ErrorActionPreference = 'Stop'
Write-Host "Exporting session..." -ForegroundColor DarkGray
$tmpFile = [System.IO.Path]::GetTempFileName() + ".json"
$env:CI = 'true'
opencode.cmd export $SessionId 2>$null | Set-Content -Path $tmpFile -NoNewline -Encoding UTF8
if (-not (Test-Path $tmpFile) -or (Get-Item $tmpFile).Length -eq 0) {
Write-Error "Export failed"
exit 1
}
$size = (Get-Item $tmpFile).Length
Write-Host "Read ($($size / 1MB -as [int]) MB)" -ForegroundColor DarkGray
$raw = Get-Content $tmpFile -Raw -Encoding UTF8
Remove-Item $tmpFile -Force
# Check if it's actually UTF-16LE in disguise (opencode export uses UTF-16LE)
if ($raw.Length -eq 0 -or $raw[0] -ne '{') {
# Re-read as UTF-16
$bytes = [System.IO.File]::ReadAllBytes($tmpFile)
Remove-Item $tmpFile -Force
$raw = [System.Text.Encoding]::Unicode.GetString($bytes)
}
$brace = $raw.IndexOf('{')
if ($brace -gt 0) { $raw = $raw.Substring($brace) }
# Use regex to find "text": "[mohe]..." patterns directly (no JSON parsing)
$pattern = '"text":\s*"\[(mohe|xxm)\][^"]*"'
$cutoff = if ($Minutes -gt 0) { (Get-Date).ToUniversalTime().AddMinutes(-$Minutes) } else { $null }
$results = @()
$matchPos = 0
while ($true) {
$m = [regex]::Match($raw, $pattern, $matchPos)
if (-not $m.Success) { break }
$matchPos = $m.Index + 1
$tag = $m.Groups[1].Value
$content = $m.Value
# Extract the full text content (everything between "text": " and the closing ")
$start = $m.Index + $m.Value.IndexOf('"', $m.Value.IndexOf(':')+1) + 1
$fullText = ''
$escape = $false
for ($i = $start; $i -lt $raw.Length; $i++) {
$c = $raw[$i]
if ($escape) { $fullText += $c; $escape = $false }
elseif ($c -eq '\') { $fullText += $c; $escape = $true }
elseif ($c -eq '"') { break }
else { $fullText += $c }
}
$fullText = $fullText.Trim()
if (-not ($fullText.StartsWith('[mohe]') -or $fullText.StartsWith('[xxm]'))) { continue }
$sender = if ($fullText.StartsWith('[mohe]')) { '[mohe] Mohe' } else { '[xxm] XiaoXiaoMo' }
$display = $fullText -replace '^\[\w+\]\s*', ''
# Get timestamp by looking backward
$before = $raw.Substring([Math]::Max(0, $m.Index - 3000), [Math]::Min(3000, $m.Index))
$tsMatch = [regex]::Match($before, '"timestamp":\s*"([^"]+)"')
$tsStr = if ($tsMatch.Success) { $tsMatch.Groups[1].Value } else { '' }
if (-not $tsStr) {
$crMatch = [regex]::Match($before, '"created":\s*(\d+)')
if ($crMatch.Success) {
$tsStr = ([DateTimeOffset]::FromUnixTimeMilliseconds([long]$crMatch.Groups[1].Value).UtcDateTime).ToString('o')
}
}
$ts = $null
if ($tsStr) {
try { $ts = [DateTime]::Parse($tsStr, $null, [System.Globalization.DateTimeStyles]::AssumeUniversal) } catch {}
}
if ($cutoff -and $ts -and $ts -lt $cutoff) { continue }
$timeLocal = if ($ts) { $ts.ToLocalTime().ToString('HH:mm:ss') } else { '??' }
$results += [PSCustomObject]@{ Time=$timeLocal; Sort=if($ts){$ts.Ticks}else{0}; Sender=$sender; Message=$display }
}
$results = $results | Sort-Object Sort
if ($results.Count -eq 0) {
Write-Host "No [mohe]/[xxm] messages found" -ForegroundColor Yellow
if ($Minutes -gt 0) { Write-Host "(last $Minutes minutes)" }
exit
}
Write-Host "`n$($results.Count) message(s)" -ForegroundColor Cyan
if ($Minutes -gt 0) { Write-Host "(last $Minutes min)" }
Write-Host ("=" * 60)
foreach ($r in $results) {
Write-Host ("[{0}] {1}: {2}" -f $r.Time, $r.Sender, $r.Message)
}
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
Mohe & XiaoXiaoMo Chat Viewer
Fixes one corrupted byte in session export, then parses cleanly.
Usage:
python moho_chat.py <session_id> [minutes]
Examples:
python moho_chat.py ses_1d95d15c4ffehQaZ6hrbIbak5k
python moho_chat.py ses_1d95d15c4ffehQaZ6hrbIbak5k 30
"""
import subprocess, sys, os, tempfile, re, json
from datetime import datetime, timezone, timedelta
def export(session_id):
"""Export session via cmd.exe (preserves UTF-16LE), return path."""
tmp = tempfile.NamedTemporaryFile(suffix='.json', delete=False)
tmp.close()
r = subprocess.run(
f'opencode.cmd export {session_id} > "{tmp.name}" 2>nul',
shell=True, timeout=180
)
if r.returncode != 0 or os.path.getsize(tmp.name) == 0:
raise ValueError('Export failed')
return tmp.name
def load(filepath):
"""Read UTF-16LE, fix corruption, parse JSON, return messages."""
with open(filepath, 'rb') as f:
raw = f.read()
# Decode UTF-16LE
if raw[:2] == b'\xff\xfe':
text = raw.decode('utf-16-le', errors='replace')
else:
text = raw.decode('utf-8', errors='replace')
# Strip to first {
brace = text.find('{')
if brace > 0:
text = text[brace:]
# FIX THE CORRUPTION:
# The first message's text field is missing its closing quote.
# Pattern: corrupt_char, comma, newline, then a field name
# Insert missing closing quote before the comma
# Specific fix: after corrupted chars that precede a structural comma
fixed = re.sub(
r'(\uFFFD)\s*,\s*\n\s*"(id|role|parts|info|timestamp|type|text)"',
r'?",\n "\2"',
text
)
# Also try without \uFFFD (in case it was decoded differently)
fixed = re.sub(
r'(\?)\s*,\s*\n\s*"(id|role|parts|info|timestamp|type|text)"',
r'?",\n "\2"',
fixed
)
return json.loads(fixed).get('messages', [])
def msg_text(msg):
parts = msg.get('parts', [])
text = ''
for p in parts:
if isinstance(p, dict) and p.get('type') == 'text':
text += p.get('text', '')
return text.strip()
def msg_ts(msg):
info = msg.get('info', {})
ts = info.get('timestamp', '') or ''
if not ts:
t = info.get('time', {})
if isinstance(t, dict) and t.get('created'):
ts = datetime.fromtimestamp(t['created']/1000, tz=timezone.utc).isoformat()
return ts
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
session_id = sys.argv[1]
minutes = int(sys.argv[2]) if len(sys.argv) > 2 else None
print('Exporting...', file=sys.stderr)
tmp = export(session_id)
try:
messages = load(tmp)
finally:
os.unlink(tmp)
print(f'{len(messages)} messages', file=sys.stderr)
# Search for 测试新协议 (Moho's test message keyword)
found_moho = 0
for m in messages:
t = msg_text(m)
if '测试新协议' in t or '写入Session' in t or '23 × 17' in t:
found_moho += 1
if found_moho <= 5:
print(f' [Moho #{found_moho}] {t[:200]!r}', file=sys.stderr)
print(f' Moho explicit messages: {found_moho}', file=sys.stderr)
cutoff = None
if minutes:
cutoff = datetime.now(timezone.utc) - timedelta(minutes=minutes)
results = []
for m in messages:
text = msg_text(m)
if not (text.startswith('[mohe]') or text.startswith('[xxm]')):
continue
sender = '[mohe] Mohe' if text.startswith('[mohe]') else '[xxm] XiaoXiaoMo'
display = re.sub(r'^\[\w+\]\s*', '', text, count=1).strip()
ts_str = msg_ts(m)
ts_dt = None
if ts_str:
try: ts_dt = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
except: pass
if cutoff and ts_dt and ts_dt < cutoff:
continue
results.append((ts_dt or datetime.min, ts_str, sender, display))
results.sort(key=lambda x: x[0])
if not results:
print('No [mohe]/[xxm] messages found' + (f' in last {minutes} min' if minutes else ''))
return
print(f'\n{len(results)} message(s)' + (f' (last {minutes} min)' if minutes else ''))
print('=' * 60)
for ts_dt, ts_str, sender, display in results:
t = '??'
if ts_str:
try: t = datetime.fromisoformat(ts_str.replace('Z', '+00:00')).strftime('%H:%M:%S')
except: t = str(ts_str)[:19]
print(f'[{t}] {sender}: {display}')
if __name__ == '__main__':
main()
-34
View File
@@ -1,34 +0,0 @@
@echo off
title WeChat Hermes Bridge
cd /d "%~dp0.."
set PYTHON=C:\Users\hmo\AppData\Local\Programs\Python\Python310\python.exe
set AGENT=scripts\wechat_agent.py
echo ========================================
echo WeChat Hermes Bridge
echo ========================================
echo.
echo [1/3] 启动微信 + 注入 DLL...
echo.
set PYTHONHOME=
set WXHOOK_LOG_LEVEL=ERROR
%PYTHON% %AGENT%
echo.
echo ========================================
echo 微信已启动!
echo.
echo 第 2 步:运行修复过低工具 ^> 扫码登录
echo 路径:D:\Program Files (x86)\低版本修复工具\
echo 低版本修复工具\修复过低6.0\低版通用杀器.sp.exe
echo.
echo 第 3 步:手机扫码 → 自动开始转发
echo.
echo 按 Ctrl+C 停止桥接
echo ========================================
if %errorlevel% neq 0 (
pause
)
+28
View File
@@ -0,0 +1,28 @@
@echo off
title WeChat History API
cd /d "%~dp0.."
set PYTHON=python
echo ========================================
echo WeChat History REST API Server
echo Port: 19001
echo ========================================
echo.
echo Starting history API server...
echo Endpoints:
echo GET http://localhost:19001/
echo GET http://localhost:19001/health
echo GET http://localhost:19001/api/contacts
echo GET http://localhost:19001/api/recent
echo GET http://localhost:19001/api/history?wxid=wxid_xxx^&count=20
echo POST http://localhost:19001/api/history
echo ========================================
echo.
set PYTHONHOME=
%PYTHON% api\history_api.py --port 19001
if %errorlevel% neq 0 (
pause
)
+189 -8
View File
@@ -5,22 +5,29 @@ import os, json, time, threading, requests, re, socketserver, subprocess, urllib
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"} # 系统账号/微信团队,不回复
WX_API = "http://127.0.0.1:19088"
LOG_FILE = r"C:\Users\hmo\Desktop\wechat_agent.log"
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
TEMP_DIR = os.path.join(PROJECT_ROOT, "temp")
LOG_FILE = os.path.join(LOG_DIR, "wechat_agent.log")
os.makedirs(LOG_DIR, exist_ok=True)
os.makedirs(TEMP_DIR, exist_ok=True)
TCP_PORT = 19099
last_msg_time = time.time()
nickname_cache = {}
db_handle_cache = None # MicroMsg.db handle for history queries
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"
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"
def log(m):
with open(LOG_FILE, "a", encoding="utf-8") as f:
@@ -37,6 +44,121 @@ def wxpost(path, data=None, timeout=10):
log(f"WX ERR: {e}")
return {"code": -1}
# ── 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
if db_handle_cache:
return db_handle_cache
r = wxpost("/api/getDBInfo", timeout=10)
dbs = r.get("data") or []
# WeChat 3.9.5.81+: messages stored in MSG0.db, MSG1.db, etc.
# Older versions: messages in MicroMsg.db's MSG table
for db in dbs:
dbname = db.get("databaseName", "")
if "MSG" in dbname or "Msg" in dbname:
db_handle_cache = db.get("handle")
log(f"History DB: {dbname} handle={db_handle_cache}")
return db_handle_cache
log("History DB handle: NOT FOUND")
return None
# Message type labels
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."""
h = get_db_handle()
if not h:
return None
# Text (type=1) and appmsg/link (type=49), use DisplayContent as fallback for StrContent
limit_val = min(int(limit), 50)
sql = f"SELECT CreateTime, IsSender, Type, SubType, StrContent, DisplayContent FROM MSG WHERE StrTalker='{wxid}' AND Type IN (1,49) ORDER BY CreateTime DESC LIMIT {limit_val}"
r = wxpost("/api/execSql", {"dbHandle": h, "sql": sql}, timeout=15)
data = r.get("data") or []
if not data or len(data) < 2:
return None
# Skip header row, reverse to chronological order
rows = data[1:]
rows.reverse()
# Normalize content: prefer StrContent, fallback to DisplayContent
results = []
for row in rows:
content = (row[4] or "").strip() if len(row) > 4 else ""
if not content and len(row) > 5:
content = (row[5] or "").strip()
if not content:
continue
results.append({"CreateTime": row[0], "IsSender": row[1], "Type": row[2], "content": content})
return results
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)}条):"]
for row in rows:
ts = int(row.get("CreateTime", 0))
time_str = time.strftime("%m/%d %H:%M", time.localtime(ts)) if ts else "?"
is_sender = int(row.get("IsSender", 0))
msg_type = int(row.get("Type", 1))
content = row.get("content", "")
# Determine who sent it
who = bot_name if is_sender else sender_name
# Format content
if msg_type == 49:
content = f"[链接] {content[:60]}"
else:
content = content[:200]
lines.append(f"[{time_str}] {who}: {content}")
return "\n".join(lines)
def handle_history(wxid, count):
"""Query and format history for a given wxid. Returns text to send."""
try:
rows = query_history(wxid, count)
if rows:
return format_history(wxid, rows)
return f"暂无与 {get_nickname(wxid)} 的聊天记录"
except Exception as e:
log(f"History ERR: {e}")
return "查询历史记录失败"
def handle_history_json(wxid, count):
"""Query history and return JSON-serializable dict for HTTP API."""
try:
rows = query_history(wxid, count)
sender_name = get_nickname(wxid)
if not rows:
return {"ok": True, "wxid": wxid, "sender_name": sender_name, "count": 0, "messages": []}
bot_name = get_nickname(BOT_WXID)
messages = []
for row in rows:
ts = int(row.get("CreateTime", 0))
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts)) if ts else ""
is_sender = int(row.get("IsSender", 0))
msg_type = int(row.get("Type", 1))
content = row.get("content", "")
messages.append({
"time": time_str,
"timestamp": ts,
"sender": bot_name if is_sender else sender_name,
"is_self": bool(is_sender),
"type": msg_type,
"content": content[:500]
})
return {
"ok": True,
"wxid": wxid,
"sender_name": sender_name,
"count": len(messages),
"requested_count": min(int(count or 10), 50),
"messages": messages
}
except Exception as e:
log(f"History JSON ERR: {e}")
return {"ok": False, "error": str(e)[:200]}
def send_wx(wxid, msg):
# Strip weixin:// URLs that WeChat interprets as commands
import re as _re2
@@ -69,6 +191,25 @@ def call_hermes(wxid, content):
log(f"API ERR: {e}")
return None
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.'"
body = {"model": "hermes-agent", "messages": [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": text}
]}
try:
r = requests.post(HERMES_API, json=body, headers=headers, timeout=180, proxies={"http": None, "https": None})
if r.status_code == 200:
reply = r.json()["choices"][0]["message"]["content"]
log(f"Inject history OK: {reply[:50]}")
return True
log(f"Inject history HTTP {r.status_code}")
except Exception as e:
log(f"Inject history ERR: {e}")
return False
# ── Inject wxhelper DLL ──
def inject_wxhelper():
try:
@@ -179,6 +320,12 @@ def process_tags(reply, fu):
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])}")
# [HISTORY:wxid:count] - query chat history from MSG table
hm = re.search(r'\[HISTORY:(\S+?):(\d+)\]', clean)
if hm:
clean = re.sub(r'\s*\[HISTORY:\S+?:\d+\]\s*', '', clean).strip()
target_wxid, count = hm.group(1), int(hm.group(2))
threading.Thread(target=lambda: send_wx(fu, handle_history(target_wxid, count)), daemon=True).start()
# [PAT:roomid:wxid]
pm = re.search(r'\[PAT:(\S+):(\S+)\]', clean)
if pm:
@@ -191,7 +338,7 @@ 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")
tmp = os.path.join(TEMP_DIR, 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)
@@ -216,7 +363,7 @@ def handle_img(m, fu):
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")
tmp = os.path.join(TEMP_DIR, 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)
@@ -225,7 +372,7 @@ def handle_img(m, fu):
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}")
tmp = os.path.join(TEMP_DIR, 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)
@@ -234,7 +381,7 @@ 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")
tmp = os.path.join(TEMP_DIR, 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)
@@ -293,11 +440,29 @@ class RH(BaseHTTPRequestHandler):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
try:
d = json.loads(body)
if self.path in ("/history", "/api/chatHistory"):
wxid = (d.get("wxid", "") or "").strip()
count = d.get("count", 10) or d.get("limit", 10)
if not wxid:
self._send_json({"ok": False, "error": "Missing wxid"})
return
self._send_json(handle_history_json(wxid, count))
return
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:
with open(os.path.join(TEMP_DIR, "hermes_inbox.txt"), "a", encoding="utf-8") as f:
f.write(f"{time.strftime('%H:%M:%S')} {msg}\n")
# HISTORY_DATA tag: query history and inject back to Hermes session
hm = re.search(r'\[HISTORY_DATA:(\S+?):(\d+)\]', msg)
if hm:
target_wxid, count = hm.group(1), int(hm.group(2))
history_text = handle_history(target_wxid, count)
if history_text:
threading.Thread(target=lambda: inject_to_hermes_session(history_text), daemon=True).start()
log(f"HISTORY_DATA: injected for {target_wxid} ({count} msgs)")
else:
log(f"HISTORY_DATA: no messages for {target_wxid}")
self.send_response(200); self.end_headers(); return
to = d.get("to", "") or d.get("wxid", "")
msg = d.get("message", "") or d.get("content", "")
@@ -308,7 +473,23 @@ class RH(BaseHTTPRequestHandler):
log(f"RH ERR: {e}")
self.send_response(200); self.end_headers()
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path in ("/history", "/api/chatHistory"):
params = parse_qs(parsed.query)
wxid = params.get("wxid", [""])[0]
count = params.get("count", ["10"])[0]
result = handle_history_json(wxid, count)
log(f"HTTP GET {parsed.path} wxid={wxid} count={count}")
self._send_json(result)
return
self.send_response(200); self.end_headers(); self.wfile.write(b'{"ok":true}')
def _send_json(self, data):
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a): pass
threading.Thread(target=lambda: HTTPServer(("0.0.0.0", 5801), RH).serve_forever(), daemon=True).start()