#!/usr/bin/env python3 """XMPP Bot zhiwei@yoin.fun - Hermes API 版(修复版:ejabberd 26.4 兼容 + 稳定重连 + 群聊配置化)""" import asyncio, logging, ssl, json, urllib.request, os, subprocess, time from xml.sax.saxutils import escape logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') GATEWAY = "http://localhost:8643/v1/chat/completions" API_KEY = "hermes123" _opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) # 读取需要加入的MUC群聊房间列表 MUC_ROOMS_FILE = "/home/hmo/.hermes/zhiwei_rooms.txt" MUC_ROOMS = [] if os.path.exists(MUC_ROOMS_FILE): with open(MUC_ROOMS_FILE) as f: MUC_ROOMS = [line.strip() for line in f if line.strip() and not line.strip().startswith('#')] logging.info(f"📋 知微待加入群聊: {MUC_ROOMS}") else: logging.warning(f"⚠️ 群聊配置文件 {MUC_ROOMS_FILE} 不存在") def send(from_jid, to_jid, body): safe = escape(body) subprocess.run(["docker","exec","ejabberd","ejabberdctl","send_message","chat", from_jid, to_jid, "", safe ], capture_output=True, timeout=10) class ZhiweiBot: def __init__(self): import slixmpp self.xmpp = slixmpp.ClientXMPP('zhiwei@yoin.fun', 'hermes123') self.xmpp.add_event_handler('session_bind', self.on_bind) self.xmpp.add_event_handler('message', self.on_msg) self.xmpp.add_event_handler('disconnected', self.on_disconnect) self.xmpp.add_event_handler('connected', self.on_connected) # 不用 slixmpp 内置自动重连(不可靠),用手动重连 self.xmpp.auto_reconnect = False ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE self.xmpp.ssl_context = ctx self.ready = asyncio.Event() self._stopped = False self._call_seq = 0 def stop(self): self._stopped = True self.xmpp.abort() async def on_connected(self, event): logging.info("🔗 知微TCP连接已建立") async def on_bind(self, event): self.xmpp.send_presence(); self.xmpp.get_roster() # 加入所有配置的MUC群聊 for room in MUC_ROOMS: try: nick = room.split('@')[0] self.xmpp.plugin['xep_0045'].join_muc(room, nick) logging.info(f"✅ 知微加入群聊 {room}") except Exception as e: logging.warning(f"⚠️ 加入群聊 {room} 失败: {e}") self.ready.set() logging.info("✅ 知微上线") async def on_disconnect(self, event): self.ready.clear() logging.warning("⚠️ 知微断线") async def on_msg(self, msg): body = msg['body']; sender = str(msg['from']); mtype = msg['type'] if not body: return # 群聊处理 if mtype == 'groupchat': nickname = sender.split('/')[-1] if '/' in sender else '' if nickname in ('hmo', 'xxm'): room = sender.split('/')[0] logging.info(f"📩 知微群消息 [{room}/{nickname}]: {body[:80]}") self._call_seq += 1 try: payload = json.dumps({ "model":"hermes-agent", "messages":[{"role":"user","content":f"[{room} {nickname}] {body}"}] }).encode() req = urllib.request.Request(GATEWAY, data=payload, method="POST") req.add_header("Content-Type","application/json") req.add_header("Authorization",f"Bearer {API_KEY}") req.add_header("X-Hermes-Session-Id","xmpp-zhiwei") loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, lambda: _opener.open(req, timeout=120)) data = json.loads(result.read()) reply = data.get("choices",[{}])[0].get("message",{}).get("content","") finish = data.get("choices",[{}])[0].get("finish_reason","") if reply.strip() and finish != "silent": self.xmpp.send_message(mto=room, mbody=reply, mtype='groupchat') logging.info(f"✅ 知微群回复: {reply[:80]}") except Exception as e: logging.error(f"❌ 知微群错误: {e}") return # 私聊处理 if mtype != 'chat': return if 'hmo@yoin.fun' in sender: self._call_seq += 1 logging.info(f"📩 老爸(#{self._call_seq}): {body}") # 先秒回确认消息,让老爸知道bot活着 send("zhiwei@yoin.fun", sender, "收到,正在思考...") try: payload = json.dumps({ "model":"hermes-agent", "messages":[{"role":"user","content":f"[zhiwei] {body}"}] }).encode() req = urllib.request.Request(GATEWAY, data=payload, method="POST") req.add_header("Content-Type","application/json") req.add_header("Authorization",f"Bearer {API_KEY}") req.add_header("X-Hermes-Session-Id","xmpp-zhiwei") loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, lambda: _opener.open(req, timeout=120)) data = json.loads(result.read()) reply = data.get("choices",[{}])[0].get("message",{}).get("content","") finish = data.get("choices",[{}])[0].get("finish_reason","") if reply.strip() and finish != "silent": send("zhiwei@yoin.fun", sender, reply) logging.info(f"✅ 知微回复: {reply[:80]}") except Exception as e: logging.error(f"❌ 知微错误: {e}") async def main(): retry_delay = 1 max_delay = 60 while True: try: z = ZhiweiBot() z.xmpp.register_plugin('xep_0030'); z.xmpp.register_plugin('xep_0199') z.xmpp.register_plugin('xep_0045') # MUC群聊支持 # XMPP Ping 每 60 秒发一次 keepalive,防服务器超时断线 z.xmpp['xep_0199'].keepalive = True z.xmpp['xep_0199'].interval = 60 z.xmpp['xep_0199'].timeout = 10 z.xmpp.connect(host='127.0.0.1', port=5222) await asyncio.wait_for(z.ready.wait(), timeout=30) logging.info("知微就绪") retry_delay = 1 # 保持运行,断线时自动跳出重连 while not z._stopped: await asyncio.sleep(3) if not z.xmpp.is_connected(): logging.warning("知微连接丢失,重连中...") break except asyncio.TimeoutError: logging.error("⏰ 知微连接超时") except Exception as e: logging.error(f"知微main错误: {e}") finally: try: if 'z' in dir() and z: z.stop() except: pass # 指数退避重连 logging.info(f"⏳ 知微等待 {retry_delay} 秒后重连...") await asyncio.sleep(retry_delay) retry_delay = min(retry_delay * 2, max_delay) if __name__ == '__main__': import sys, os, signal # PID 文件防多进程:启动时检查,如果已有实例则杀旧启新 PIDFILE = '/home/hmo/.hermes/zhiwei_bot.pid' if os.path.exists(PIDFILE): try: old_pid = int(open(PIDFILE).read().strip()) os.kill(old_pid, signal.SIGTERM) print(f"Killed old bot process {old_pid}") import time time.sleep(2) except (ValueError, ProcessLookupError, OSError): pass with open(PIDFILE, 'w') as f: f.write(str(os.getpid())) try: asyncio.run(main()) except KeyboardInterrupt: pass finally: if os.path.exists(PIDFILE): os.remove(PIDFILE)