From 3a14d776c0883dcc3bedbdc137be716afa48b194 Mon Sep 17 00:00:00 2001 From: mohe Date: Sat, 13 Jun 2026 01:18:50 +0800 Subject: [PATCH] =?UTF-8?q?zhiwei=20bot:=20=E6=B7=BB=E5=8A=A0MUC=E7=BE=A4?= =?UTF-8?q?=E8=81=8A=E6=94=AF=E6=8C=81=EF=BC=88=E9=85=8D=E7=BD=AE=E5=8C=96?= =?UTF-8?q?=E6=88=BF=E9=97=B4=E5=88=97=E8=A1=A8=20+=20=E7=BE=A4=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=94=B6=E5=8F=91=20+=20=E7=A8=B3=E5=AE=9A=E9=87=8D?= =?UTF-8?q?=E8=BF=9E=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改动内容: - 从配置文件 zhiwei_rooms.txt 读取待加入群聊列表 - 注册 xep_0045 插件,on_bind 时自动加入所有配置房间 - on_msg 处理 groupchat 类型,只响应 hmo/xxm - 群聊回复使用 xmpp.send_message(mtype='groupchat') - 改为手动重连(auto_reconnect=False),指数退避 - 添加 xep_0199 keepalive(60s 间隔)防断线 - 私聊添加秒回确认 '收到,正在思考...' - 超时从 600s 改为 120s - __main__ 添加 PID 文件防多进程 --- xmpp_zhiwei_bot.py | 100 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/xmpp_zhiwei_bot.py b/xmpp_zhiwei_bot.py index 56cbe71..7ddb5f5 100644 --- a/xmpp_zhiwei_bot.py +++ b/xmpp_zhiwei_bot.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""XMPP Bot zhiwei@yoin.fun - Hermes API 版(修复版:ejabberd 26.4 兼容 + 稳定重连)""" +"""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 @@ -8,6 +8,16 @@ 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", @@ -22,8 +32,8 @@ class ZhiweiBot: 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 = True + # 不用 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() @@ -38,7 +48,16 @@ class ZhiweiBot: logging.info("🔗 知微TCP连接已建立") async def on_bind(self, event): - self.xmpp.send_presence(); self.xmpp.get_roster(); self.ready.set() + 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): @@ -46,11 +65,45 @@ class ZhiweiBot: logging.warning("⚠️ 知微断线") async def on_msg(self, msg): - body = msg['body']; sender = str(msg['from']) - if not body or msg['type'] != 'chat': return + 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", @@ -61,7 +114,7 @@ class ZhiweiBot: 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=600)) + 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","") @@ -78,14 +131,22 @@ async def main(): 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 - # 保持运行 — slixmpp 内置 auto_reconnect 会自动处理断线重连 + # 保持运行,断线时自动跳出重连 while not z._stopped: - await asyncio.sleep(5) + await asyncio.sleep(3) + if not z.xmpp.is_connected(): + logging.warning("知微连接丢失,重连中...") + break except asyncio.TimeoutError: logging.error("⏰ 知微连接超时") @@ -104,7 +165,28 @@ async def main(): 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)