refactor(xxm): consolidate 4 bot implementations into unified xmpp_agent_core.py

- Merge bot_base.py, gateway/scripts/xmpp_bot.py, bots/*, xmpp_bot_rest.py
  into single xmpp_agent_core.py with --agent flag (xxm|mohe|zhiwei|xiaoguo)
- Add xxm_bot.py wrapper (encoding=utf-8 for Windows exec)
- Fix slixmpp connect() API: use host=/port= keyword args (was tuple)
- Clean up orphans: bots/, scripts/, hermes_state.py, xmpp_bot.py, xmpp_bot_rest.py
- Add docs/CLEANUP_PLAN.md documenting the migration
- Update README.md project structure
- Also: fix WeChat agent path resolution (relative paths)
This commit is contained in:
hmo
2026-06-21 16:13:57 +08:00
parent b9df510f31
commit babbc46801
22 changed files with 1273 additions and 5442 deletions
+78 -3
View File
@@ -21,7 +21,8 @@ if not _lock.ok:
BOT_WXID = "wxid_5bhmquvkbude22"
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__)))
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
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")
@@ -155,8 +156,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\Injector_x64.exe"
WXHELPER_DLL = r"D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway\tools\wxhelper_official_39581.dll"
INJECTOR = os.path.join(SCRIPT_DIR, "..", "tools", "Injector_x64.exe")
WXHELPER_DLL = os.path.join(SCRIPT_DIR, "..", "tools", "wxhelper_official_39581.dll")
def log(m):
with open(LOG_FILE, "a", encoding="utf-8") as f:
@@ -569,8 +570,24 @@ def process_msg(raw_data):
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)
# DEBUG: capture Type 49 full XML for URL analysis
if msg_type == 49:
try:
with open(LOG_DIR + "/t49_xml.txt", "a", encoding="utf-8") as _f:
_f.write(f"\n=== {time.time()} type=49 from={fu} ===\n{ct[:10000]}\n")
except: pass
if "@chatroom" in fu:
log(f"GROUP RAW DUMP: keys={list(d.keys())} ct_len={len(ct)} ct[:100]={ct[:100]}")
# DEBUG: capture full raw data for quote analysis
try:
with open(LOG_DIR + "/group_raw.jsonl", "a", encoding="utf-8") as _f:
_f.write(json.dumps({k: str(v)[:2000] for k, v in d.items()}, ensure_ascii=False) + "\n")
except: pass
# DEBUG: capture all raw msgs for field analysis
try:
with open(LOG_DIR + "/all_raw.jsonl", "a", encoding="utf-8") as _f:
_f.write(json.dumps({k: str(v)[:500] for k, v in d.items()}, ensure_ascii=False) + "\n")
except: pass
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
@@ -608,6 +625,64 @@ def process_msg(raw_data):
else:
log(f"-> {fu}: skip (blank image response)")
return
# Type 49 (forwarded article) - extract URL and process via article_processor
if msg_type == 49 and ct.strip().startswith("<?xml"):
try:
import re
# Try <url> first, then <shareUrlOriginal>, then <shareUrlOpen>
urls = re.findall(r'<url>(https?://mp\.weixin\.qq\.com[^<]+)</url>', ct)
if not urls:
urls = re.findall(r'<shareUrlOriginal>(https?://mp\.weixin\.qq\.com[^<]+)</shareUrlOriginal>', ct)
if not urls:
urls = re.findall(r'<shareUrlOpen>(https?://mp\.weixin\.qq\.com[^<]+)</shareUrlOpen>', ct)
url = urls[0] if urls else None
# Extract title from XML
titles = re.findall(r'<title>(.*?)</title>', ct)
title = titles[0] if titles else ""
# Extract description
descs = re.findall(r'<des>(.*?)</des>', ct)
desc = descs[0] if descs else ""
if url:
log(f"ARTICLE URL: {url}")
# Call article_processor on localhost
import urllib.request as ur
req = ur.Request("http://127.0.0.1:5810/process",
data=json.dumps({"url": url}).encode("utf-8"),
headers={"Content-Type": "application/json"})
with ur.urlopen(req, timeout=180) as resp:
result = json.loads(resp.read().decode("utf-8"))
if result.get("status") == "ok":
content = result.get("content", "")[:3000]
title = result.get("title", "")
images = result.get("images_ocr", 0)
enriched = f"[老莫转发了一篇文章{(chr(10)+'标题: '+title) if title else ''}{images}张图片已OCR]\n\n{content}"
log(f"ARTICLE processed: {len(content)} chars")
reply = call_hermes(fu, enriched)
if reply and reply.strip():
log(f"-> {fu}: {reply[:50]}")
send_wx(fu, reply.strip())
return
else:
log(f"ARTICLE process failed: {result.get('error','')[:100]}")
# Fallback: send title + description
fallback = f"[老莫转发了一篇文章]{(chr(10)+'标题: '+title) if title else ''}{(chr(10)+'摘要: '+desc[:200]) if desc else ''}\n(全文抓取失败: {result.get('error','')[:60]})"
reply = call_hermes(fu, fallback)
if reply and reply.strip():
send_wx(fu, reply.strip())
return
else:
# No URL found, send title + description
if title:
log(f"ARTICLE: no URL, sending title+desc")
fallback = f"[老莫转发了一篇文章]{(chr(10)+'标题: '+title) if title else ''}{(chr(10)+'摘要: '+desc[:200]) if desc else ''}"
reply = call_hermes(fu, fallback)
if reply and reply.strip():
send_wx(fu, reply.strip())
return
except Exception as e:
log(f"ARTICLE handler error: {e}")
# Fall through to text handler
# Text - prepend sender wxid+name so Hermes knows who's talking
sender_name = get_nickname(fu)
chat_type = "Group" if "@chatroom" in fu else "Private"
+18 -19
View File
@@ -26,8 +26,8 @@ if not _lock.ok:
# ── Config ──
JID = "xxm@yoin.fun"
PASSWORD = "hermes123"
SERVER = "xmpp.yoin.fun"
PORT = 3021
SERVER = "192.168.1.246"
PORT = 5222
ATTACH_SESSION = "ses_xxm_xmpp"
MUC_ROOMS = [
"coregroup@conference.yoin.fun", # core group chat
@@ -696,23 +696,22 @@ if __name__ == "__main__":
bot_nick = JID.split("@")[0]
async def _join_silent():
for room_jid in MUC_ROOMS:
for attempt in range(3):
try:
# Use join_muc_wait to ensure room join completes
await self.plugin['xep_0045'].join_muc_wait(room_jid, bot_nick, timeout=60)
log(f"Joined {room_jid} (silent)")
break
except asyncio.TimeoutError:
log(f"MUC join timeout ({attempt+1}/3) for {room_jid}")
if attempt == 2:
log(f"MUC setup failed for {room_jid} after 3 attempts")
await asyncio.sleep(5)
else:
await asyncio.sleep(3)
except Exception as e:
log(f"MUC setup failed for {room_jid}: {e} (type={type(e).__name__})")
await asyncio.sleep(5)
break
nick = bot_nick
try:
# Use join_muc (non-waiting) to register plugin state
self.plugin['xep_0045'].join_muc(room_jid, nick)
# Also send raw presence as backup
presence = (
f"<presence to='{room_jid}/{nick}'>"
f"<x xmlns='http://jabber.org/protocol/muc'>"
f"<history maxstanzas='0'/>"
f"</x></presence>"
)
self.send_raw(presence)
log(f"Joined {room_jid} (async)")
except Exception as e:
log(f"MUC join failed for {room_jid}: {type(e).__name__}: {e}")
await asyncio.sleep(2)
# After joining, query MAM for recent history
await asyncio.sleep(3) # wait for MUC join to propagate
await _fetch_mam_history()