From 2d0f3906573764994fe96db7d103c142c22cda7d Mon Sep 17 00:00:00 2001 From: mohe Date: Sat, 20 Jun 2026 01:05:01 +0800 Subject: [PATCH] unify xmpp bots: single xmpp_agent_core.py + --agent param wrappers --- xmpp_agent_core.py | 269 ++++++++++++++++++++++++++++++++++++++++++++ xmpp_bot.py | 212 +++++++++++++++++++++++++++------- xmpp_mohe_bot.py | 5 + xmpp_xiaoguo_bot.py | 138 +---------------------- xmpp_zhiwei_bot.py | 195 +------------------------------- 5 files changed, 451 insertions(+), 368 deletions(-) create mode 100644 xmpp_agent_core.py create mode 100644 xmpp_mohe_bot.py diff --git a/xmpp_agent_core.py b/xmpp_agent_core.py new file mode 100644 index 0000000..7d5f186 --- /dev/null +++ b/xmpp_agent_core.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +"""XMPP Bot - 统一版,支持 --agent mohe|zhiwei|xiao 参数""" +import asyncio, logging, ssl, json, urllib.request, os, time, sys +from slixmpp import ClientXMPP + +# ── Agent 配置 ────────────────────────────────────────────── +AGENTS = { + "mohe": { + "jid": "mohe@yoin.fun", + "password": "hermes123", + "nick": "mohe", + "name_cn": "莫荷", + "http_port": 5804, + "gateway": "http://localhost:8642/v1/chat/completions", + "session_id": "xmpp-mohe-v2", + "mention": "@mohe/@莫荷", + }, + "zhiwei": { + "jid": "zhiwei@yoin.fun", + "password": "hermes123", + "nick": "zhiwei", + "name_cn": "知微", + "http_port": 5805, + "gateway": "http://localhost:8643/v1/chat/completions", + "session_id": "xmpp-zhiwei", + "mention": "@zhiwei/@知微", + }, + "xiaoguo": { + "jid": "xiaoguo@yoin.fun", + "password": "hermes123", + "nick": "xiaoguo", + "name_cn": "小果", + "http_port": 5806, + "gateway": "http://localhost:8645/v1/chat/completions", + "session_id": "xmpp-xiaoguo", + "mention": "@xiaoguo/@小果", + }, +} + +agent = sys.argv[sys.argv.index("--agent") + 1] if "--agent" in sys.argv else "mohe" +cfg = AGENTS.get(agent, AGENTS["mohe"]) + +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') +GATEWAY = cfg["gateway"] +API_KEY = "hermes123" +AGENT_NICK = cfg["nick"] +AGENT_NAME = cfg["name_cn"] +AGENT_JID = cfg["jid"] +AGENT_MENTION = cfg["mention"] +SESSION_ID = cfg["session_id"] +HTTP_PORT = cfg["http_port"] +_opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + +# ── HTTP 桥(接收本地脚本的主动发送请求) ── +from http.server import HTTPServer, BaseHTTPRequestHandler +import threading, json as json_mod + +_send_queue = [] + +class SendHandler(BaseHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(length) + try: + data = json_mod.loads(body) + room = data.get('to', 'coregroup@conference.yoin.fun') + text = data.get('body', '') + if text: + _send_queue.append((room, text)) + self.send_response(200) + self.end_headers() + self.wfile.write(b'{"ok":true}') + else: + self.send_response(400) + self.end_headers() + self.wfile.write(b'{"ok":false,"error":"empty body"}') + except Exception as e: + self.send_response(500) + self.end_headers() + self.wfile.write(f'{{"ok":false,"error":"{e}"}}'.encode()) + +def _run_http(): + server = HTTPServer(('127.0.0.1', HTTP_PORT), SendHandler) + server.timeout = 1.0 + while True: + server.handle_request() + +threading.Thread(target=_run_http, daemon=True).start() +logging.info(f"🚀 {AGENT_NAME} HTTP 桥启动于 :{HTTP_PORT}") + +# ── XMPP Bot 类 ──────────────────────────────────────────────── +class AgentBot(ClientXMPP): + def __init__(self): + super().__init__(AGENT_JID, cfg["password"]) + self.add_event_handler('session_bind', self.on_bind) + self.add_event_handler('message', self.on_msg) + self.add_event_handler('disconnected', self.on_disconnect) + self.add_event_handler('connected', self.on_connected) + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + self.ssl_context = ctx + self.ready = asyncio.Event() + self._call_seq = 0 + self._muc_joined = False + self._recent_sent = [] + + async def on_connected(self, event): + logging.info(f"🔗 {AGENT_NAME} TCP连接已建立") + + async def on_bind(self, event): + self.send_presence() + self.get_roster() + try: + self.plugin['xep_0045'].join_muc('coregroup@conference.yoin.fun', AGENT_NICK) + logging.info(f"✅ {AGENT_NAME} 加入群聊 coregroup") + except Exception as e: + logging.error(f"❌ {AGENT_NAME} 加入群聊失败: {e}") + self._muc_joined = True + self.ready.set() + logging.info(f"✅ {AGENT_NAME} XMPP 上线") + + async def on_disconnect(self, event): + self.ready.clear() + self._muc_joined = False + logging.warning(f"⚠️ {AGENT_NAME} XMPP 断线") + + async def on_msg(self, msg): + body = msg['body'] + sender = str(msg['from']) + msg_type = msg['type'] + if not body: + return + if msg_type == 'groupchat': + if AGENT_JID in sender: + return + nickname = sender.split('/')[-1] if '/' in sender else '' + # 自己的消息跳过(通过昵称) + if nickname == AGENT_NICK: + return + + # 硬闭嘴闸门:hmo 说闭嘴类的话 → 静默 5 分钟 + _silent_until = getattr(self, '_silent_until', 0) + if time.time() < _silent_until: + return + if nickname == 'hmo': + _sk = ['闭嘴', '别说话', '安静', 'shut', 'stfu', '别说了', '停'] + if any(kw in body.lower() for kw in _sk): + self._silent_until = time.time() + 300 + logging.info(f"🔇 {AGENT_NAME} 收到闭嘴指令,静默 5 分钟") + return + + logging.info(f"📩 群消息 [{sender}]: {body[:100]}") + room = sender.split('/')[0] + ctx_body = ( + "【规则】以下是一条群聊消息。判断是否应该回复。\n" + "只有以下3种情况你才回复:\n" + f"1. hmo直接点名问你({AGENT_MENTION})\n" + "2. 你有其他人没说过的独家信息\n" + "3. 别人说错了关键事实,不纠正会有后果\n" + "如果以上都不符合,你的回复必须只包含 __SILENT__ 这10个字符," + "不要有任何其他内容(不要前缀、不要解释、不要标点、不要空格)。\n\n" + f"[核心群 {room}] {nickname} 说: {body}" + ) + await self.call_hermes(ctx_body, room, is_group=True) + return + if msg_type == 'chat' and 'hmo@yoin.fun' in sender: + self._call_seq += 1 + logging.info(f"📩 老爸(#{self._call_seq}): {body}") + await self.call_hermes(body, sender, seq=self._call_seq) + + async def call_hermes(self, content, sender, is_group=False, seq=None): + msg_type = 'groupchat' if is_group else 'chat' + try: + payload = json.dumps({ + "model": "hermes-agent", + "messages": [{"role": "user", "content": content}] + }).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", SESSION_ID) + + loop = asyncio.get_event_loop() + result = await loop.run_in_executor(None, lambda: _opener.open(req, timeout=600)) + + if seq is not None and seq < self._call_seq: + return + + data = json.loads(result.read()) + reply = data.get("choices", [{}])[0].get("message", {}).get("content", "") + reply_stripped = reply.strip() + if reply_stripped.startswith('__SILENT__') or reply_stripped.startswith('`__SILENT__`'): + logging.info(f"⏭️ {AGENT_NAME} 决定沉默,不发送") + return + finish = data.get("choices", [{}])[0].get("finish_reason", "") + + if reply.strip() and finish != "silent": + if msg_type == 'groupchat': + self.send_message(mto=sender, mbody=reply, mtype='groupchat') + sent_norm = reply.strip()[:100] + self._recent_sent.append(sent_norm) + if len(self._recent_sent) > 10: + self._recent_sent.pop(0) + else: + import subprocess as sp + from xml.sax.saxutils import escape + safe = escape(reply) + sp.run([ + "docker", "exec", "ejabberd", "ejabberdctl", "send_stanza", + AGENT_JID, str(sender), + f"{safe}" + ], capture_output=True, timeout=10) + logging.info(f"✅ {AGENT_NAME} 回复: {reply[:80]}") + except Exception as e: + logging.error(f"❌ {AGENT_NAME} 错误: {e}") + +# ── 主入口 ─────────────────────────────────────────────── +async def main(): + retry_delay = 1 + max_delay = 60 + while True: + try: + bot = AgentBot() + bot.register_plugin('xep_0030') + bot.register_plugin('xep_0045') + bot.register_plugin('xep_0199') + + bot.connect(host='127.0.0.1', port=5222) + await asyncio.wait_for(bot.ready.wait(), timeout=30) + logging.info(f"{AGENT_NAME} XMPP 就绪") + retry_delay = 1 + + async def _drain_queue(): + while True: + await asyncio.sleep(1) + while _send_queue: + room, text = _send_queue.pop(0) + try: + bot.send_message(mto=room, mbody=text, mtype='groupchat') + sent_norm = text.strip()[:100] + bot._recent_sent.append(sent_norm) + if len(bot._recent_sent) > 10: + bot._recent_sent.pop(0) + logging.info(f"📤 主动发送到 {room}: {text[:60]}") + except Exception as e: + logging.error(f"❌ 主动发送失败: {e}") + asyncio.create_task(_drain_queue()) + + while True: + await asyncio.sleep(15) + if not bot.is_connected(): + logging.warning("检测到断线,准备重连...") + break + + except asyncio.TimeoutError: + logging.warning("连接超时,准备重连...") + except Exception as e: + logging.error(f"❌ 主循环错误: {e}") + + logging.info(f"⏳ 等待 {retry_delay} 秒后重连...") + await asyncio.sleep(retry_delay) + retry_delay = min(retry_delay * 2, max_delay) + +if __name__ == '__main__': + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass diff --git a/xmpp_bot.py b/xmpp_bot.py index eb76f7c..7d5f186 100644 --- a/xmpp_bot.py +++ b/xmpp_bot.py @@ -1,16 +1,97 @@ #!/usr/bin/env python3 -"""XMPP Bot mohe@yoin.fun - 稳定重连版""" -import asyncio, logging, ssl, json, urllib.request, os, time, re +"""XMPP Bot - 统一版,支持 --agent mohe|zhiwei|xiao 参数""" +import asyncio, logging, ssl, json, urllib.request, os, time, sys from slixmpp import ClientXMPP +# ── Agent 配置 ────────────────────────────────────────────── +AGENTS = { + "mohe": { + "jid": "mohe@yoin.fun", + "password": "hermes123", + "nick": "mohe", + "name_cn": "莫荷", + "http_port": 5804, + "gateway": "http://localhost:8642/v1/chat/completions", + "session_id": "xmpp-mohe-v2", + "mention": "@mohe/@莫荷", + }, + "zhiwei": { + "jid": "zhiwei@yoin.fun", + "password": "hermes123", + "nick": "zhiwei", + "name_cn": "知微", + "http_port": 5805, + "gateway": "http://localhost:8643/v1/chat/completions", + "session_id": "xmpp-zhiwei", + "mention": "@zhiwei/@知微", + }, + "xiaoguo": { + "jid": "xiaoguo@yoin.fun", + "password": "hermes123", + "nick": "xiaoguo", + "name_cn": "小果", + "http_port": 5806, + "gateway": "http://localhost:8645/v1/chat/completions", + "session_id": "xmpp-xiaoguo", + "mention": "@xiaoguo/@小果", + }, +} + +agent = sys.argv[sys.argv.index("--agent") + 1] if "--agent" in sys.argv else "mohe" +cfg = AGENTS.get(agent, AGENTS["mohe"]) + logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') -GATEWAY = "http://localhost:8642/v1/chat/completions" +GATEWAY = cfg["gateway"] API_KEY = "hermes123" +AGENT_NICK = cfg["nick"] +AGENT_NAME = cfg["name_cn"] +AGENT_JID = cfg["jid"] +AGENT_MENTION = cfg["mention"] +SESSION_ID = cfg["session_id"] +HTTP_PORT = cfg["http_port"] _opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) -class MoheBot(ClientXMPP): +# ── HTTP 桥(接收本地脚本的主动发送请求) ── +from http.server import HTTPServer, BaseHTTPRequestHandler +import threading, json as json_mod + +_send_queue = [] + +class SendHandler(BaseHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(length) + try: + data = json_mod.loads(body) + room = data.get('to', 'coregroup@conference.yoin.fun') + text = data.get('body', '') + if text: + _send_queue.append((room, text)) + self.send_response(200) + self.end_headers() + self.wfile.write(b'{"ok":true}') + else: + self.send_response(400) + self.end_headers() + self.wfile.write(b'{"ok":false,"error":"empty body"}') + except Exception as e: + self.send_response(500) + self.end_headers() + self.wfile.write(f'{{"ok":false,"error":"{e}"}}'.encode()) + +def _run_http(): + server = HTTPServer(('127.0.0.1', HTTP_PORT), SendHandler) + server.timeout = 1.0 + while True: + server.handle_request() + +threading.Thread(target=_run_http, daemon=True).start() +logging.info(f"🚀 {AGENT_NAME} HTTP 桥启动于 :{HTTP_PORT}") + +# ── XMPP Bot 类 ──────────────────────────────────────────────── +class AgentBot(ClientXMPP): def __init__(self): - super().__init__('mohe@yoin.fun', 'hermes123') + super().__init__(AGENT_JID, cfg["password"]) self.add_event_handler('session_bind', self.on_bind) self.add_event_handler('message', self.on_msg) self.add_event_handler('disconnected', self.on_disconnect) @@ -22,23 +103,27 @@ class MoheBot(ClientXMPP): self.ready = asyncio.Event() self._call_seq = 0 self._muc_joined = False + self._recent_sent = [] async def on_connected(self, event): - logging.info("🔗 TCP连接已建立") + logging.info(f"🔗 {AGENT_NAME} TCP连接已建立") async def on_bind(self, event): self.send_presence() self.get_roster() - # 加入内核组(每次重连后重新加入) - self.plugin['xep_0045'].join_muc('coregroup@conference.yoin.fun', 'mohe') + try: + self.plugin['xep_0045'].join_muc('coregroup@conference.yoin.fun', AGENT_NICK) + logging.info(f"✅ {AGENT_NAME} 加入群聊 coregroup") + except Exception as e: + logging.error(f"❌ {AGENT_NAME} 加入群聊失败: {e}") self._muc_joined = True self.ready.set() - logging.info("✅ 莫荷 XMPP 上线") + logging.info(f"✅ {AGENT_NAME} XMPP 上线") async def on_disconnect(self, event): self.ready.clear() self._muc_joined = False - logging.warning("⚠️ XMPP 断线") + logging.warning(f"⚠️ {AGENT_NAME} XMPP 断线") async def on_msg(self, msg): body = msg['body'] @@ -47,14 +132,37 @@ class MoheBot(ClientXMPP): if not body: return if msg_type == 'groupchat': - if 'mohe@yoin.fun' in sender: + if AGENT_JID in sender: return nickname = sender.split('/')[-1] if '/' in sender else '' - if nickname in ('hmo', 'xxm'): - logging.info(f"📩 群消息 [{sender}]: {body[:100]}") - room = sender.split('/')[0] - ctx_body = f"[核心群 {room}] {nickname} 说: {body}" - await self.call_hermes(ctx_body, room, is_group=True) + # 自己的消息跳过(通过昵称) + if nickname == AGENT_NICK: + return + + # 硬闭嘴闸门:hmo 说闭嘴类的话 → 静默 5 分钟 + _silent_until = getattr(self, '_silent_until', 0) + if time.time() < _silent_until: + return + if nickname == 'hmo': + _sk = ['闭嘴', '别说话', '安静', 'shut', 'stfu', '别说了', '停'] + if any(kw in body.lower() for kw in _sk): + self._silent_until = time.time() + 300 + logging.info(f"🔇 {AGENT_NAME} 收到闭嘴指令,静默 5 分钟") + return + + logging.info(f"📩 群消息 [{sender}]: {body[:100]}") + room = sender.split('/')[0] + ctx_body = ( + "【规则】以下是一条群聊消息。判断是否应该回复。\n" + "只有以下3种情况你才回复:\n" + f"1. hmo直接点名问你({AGENT_MENTION})\n" + "2. 你有其他人没说过的独家信息\n" + "3. 别人说错了关键事实,不纠正会有后果\n" + "如果以上都不符合,你的回复必须只包含 __SILENT__ 这10个字符," + "不要有任何其他内容(不要前缀、不要解释、不要标点、不要空格)。\n\n" + f"[核心群 {room}] {nickname} 说: {body}" + ) + await self.call_hermes(ctx_body, room, is_group=True) return if msg_type == 'chat' and 'hmo@yoin.fun' in sender: self._call_seq += 1 @@ -71,67 +179,85 @@ class MoheBot(ClientXMPP): 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-mohe-v2") - + req.add_header("X-Hermes-Session-Id", SESSION_ID) + loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, lambda: _opener.open(req, timeout=600)) - + if seq is not None and seq < self._call_seq: return - + data = json.loads(result.read()) reply = data.get("choices", [{}])[0].get("message", {}).get("content", "") - # 处理 __SILENT__ 和 __REPLY__ 标记 - if reply.strip().startswith('__SILENT__'): - logging.info("⏭️ 决定沉默,不发送") + reply_stripped = reply.strip() + if reply_stripped.startswith('__SILENT__') or reply_stripped.startswith('`__SILENT__`'): + logging.info(f"⏭️ {AGENT_NAME} 决定沉默,不发送") return - reply = re.sub(r'^__REPLY__\s*', '', reply) finish = data.get("choices", [{}])[0].get("finish_reason", "") - + if reply.strip() and finish != "silent": if msg_type == 'groupchat': self.send_message(mto=sender, mbody=reply, mtype='groupchat') + sent_norm = reply.strip()[:100] + self._recent_sent.append(sent_norm) + if len(self._recent_sent) > 10: + self._recent_sent.pop(0) else: import subprocess as sp from xml.sax.saxutils import escape safe = escape(reply) sp.run([ "docker", "exec", "ejabberd", "ejabberdctl", "send_stanza", - "mohe@yoin.fun", str(sender), - f"{safe}" + AGENT_JID, str(sender), + f"{safe}" ], capture_output=True, timeout=10) - logging.info(f"✅ 回复: {reply[:80]}") + logging.info(f"✅ {AGENT_NAME} 回复: {reply[:80]}") except Exception as e: - logging.error(f"❌ 错误: {e}") + logging.error(f"❌ {AGENT_NAME} 错误: {e}") +# ── 主入口 ─────────────────────────────────────────────── async def main(): - retry_delay = 1 # 初始重试间隔(秒) - max_delay = 60 # 最大重试间隔 + retry_delay = 1 + max_delay = 60 while True: try: - bot = MoheBot() - bot.register_plugin('xep_0030') # Service Discovery - bot.register_plugin('xep_0045') # MUC - bot.register_plugin('xep_0199') # XMPP Ping(保活) - + bot = AgentBot() + bot.register_plugin('xep_0030') + bot.register_plugin('xep_0045') + bot.register_plugin('xep_0199') + bot.connect(host='127.0.0.1', port=5222) await asyncio.wait_for(bot.ready.wait(), timeout=30) - logging.info("莫荷 XMPP 就绪") - retry_delay = 1 # 连接成功后重置重试间隔 - - # 保持运行,断线时自动重连 + logging.info(f"{AGENT_NAME} XMPP 就绪") + retry_delay = 1 + + async def _drain_queue(): + while True: + await asyncio.sleep(1) + while _send_queue: + room, text = _send_queue.pop(0) + try: + bot.send_message(mto=room, mbody=text, mtype='groupchat') + sent_norm = text.strip()[:100] + bot._recent_sent.append(sent_norm) + if len(bot._recent_sent) > 10: + bot._recent_sent.pop(0) + logging.info(f"📤 主动发送到 {room}: {text[:60]}") + except Exception as e: + logging.error(f"❌ 主动发送失败: {e}") + asyncio.create_task(_drain_queue()) + while True: await asyncio.sleep(15) if not bot.is_connected(): logging.warning("检测到断线,准备重连...") break - + except asyncio.TimeoutError: logging.warning("连接超时,准备重连...") except Exception as e: logging.error(f"❌ 主循环错误: {e}") - - # 指数退避重连:1s → 2s → 4s → 8s → ... → 60s max + logging.info(f"⏳ 等待 {retry_delay} 秒后重连...") await asyncio.sleep(retry_delay) retry_delay = min(retry_delay * 2, max_delay) diff --git a/xmpp_mohe_bot.py b/xmpp_mohe_bot.py new file mode 100644 index 0000000..68f3f28 --- /dev/null +++ b/xmpp_mohe_bot.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +"""Wrapper for xmpp_agent_core.py --agent mohe""" +import sys, os +sys.argv = [sys.argv[0], '--agent', 'mohe'] +exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py')).read()) diff --git a/xmpp_xiaoguo_bot.py b/xmpp_xiaoguo_bot.py index 5dbd96a..fe8db77 100644 --- a/xmpp_xiaoguo_bot.py +++ b/xmpp_xiaoguo_bot.py @@ -1,135 +1,5 @@ #!/usr/bin/env python3 -"""XMPP Bot xiaoguo@yoin.fun - 跑在 Linux 上""" -import asyncio, logging, ssl, json, urllib.request, subprocess, re -from xml.sax.saxutils import escape - -logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') -GATEWAY = "http://localhost:8645/v1/chat/completions" -API_KEY = "hermes123" -_opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) - -def send(from_jid, to_jid, body): - safe = escape(body) - subprocess.run(["docker","exec","ejabberd","ejabberdctl","send_stanza", - from_jid, to_jid, - f"{safe}" - ], capture_output=True, timeout=10) - -class XiaoGuoBot: - def __init__(self): - import slixmpp - self.xmpp = slixmpp.ClientXMPP('xiaoguo@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) - 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._call_seq = 0 - - async def on_bind(self, event): - self.xmpp.send_presence() - self.xmpp.get_roster() - # 加入内核组 - self.xmpp.plugin['xep_0045'].join_muc('coregroup@conference.yoin.fun', 'xiaoguo') - 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']) - msg_type = msg['type'] - if not body: - return - # 群聊 - if msg_type == 'groupchat': - if 'xiaoguo@yoin.fun' in sender: - return - nickname = sender.split('/')[-1] if '/' in sender else '' - if nickname in ('hmo', 'xxm'): - logging.info(f"📩 群消息 [{sender}]: {body[:80]}") - room = sender.split('/')[0] - ctx_body = f"[核心群 {room}] {nickname} 说: {body}" - await self.call_hermes(ctx_body, room, is_group=True) - return - # 私聊 - if msg_type == 'chat' and 'hmo@yoin.fun' in sender: - self._call_seq += 1 - logging.info(f"📩 老爸(#{self._call_seq}): {body}") - await self.call_hermes(body, sender) - - async def call_hermes(self, content, sender, is_group=False): - msg_type = 'groupchat' if is_group else 'chat' - try: - payload = json.dumps({ - "model": "hermes-agent", - "messages": [{"role": "user", "content": f"[xiaoguo] {content}"}] - }).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-xiaoguo") - loop = asyncio.get_event_loop() - result = await loop.run_in_executor(None, lambda: _opener.open(req, timeout=600)) - data = json.loads(result.read()) - reply = data.get("choices", [{}])[0].get("message", {}).get("content", "") - finish = data.get("choices", [{}])[0].get("finish_reason", "") - stripped = reply.strip() - # 处理 __SILENT__ 前缀(bot直接丢弃不发送) - if stripped.startswith('__SILENT__'): - logging.info("⏭️ 小果决定沉默,不发送") - return - # LLM自认为silent时直接丢弃 - if finish == "silent": - logging.info(f"⏭️ LLM finish=silent,丢弃回复: {stripped[:60]}") - return - # 安全网:过滤沉默宣告/纯确认/自我宣告类文本(覆盖更多变体) - SILENCE_RE = re.compile( - r'^(' - r'收到[了]?|明白[了]?|好的?[吧]?|嗯[嗯]*|哦[哦]*|OK\b|ok\b|okay\b|知道[了]?|来了|在[的呢]?|是[的]?' - r'|安静|沉默|闭嘴|不插嘴|我沉默了|保持安静|保持沉默|我先闭嘴' - r'|先.?安静|先.?观察|我.?安静|我.?沉默|我.?闭嘴|我先.?[不没]|我先看看|我观察' - r'|收到[,。!?,.!?\s]+.*$' - r')' - r'[,。!?,.!?\s]*$', re.IGNORECASE - ) - if SILENCE_RE.match(stripped): - logging.info(f"⏭️ 小果沉默宣告被拦截: {stripped[:60]}") - return - # 处理 __REPLY__ 前缀 - reply = re.sub(r'^__REPLY__\s*', '', reply) - # 空回复或纯空白不发送 - if not reply.strip(): - return - # 最终发送 - if is_group: - self.xmpp.send_message(mto=sender, mbody=reply, mtype='groupchat') - else: - send("xiaoguo@yoin.fun", sender, reply) - logging.info(f"✅ 小果回复: {reply[:80]}") - except Exception as e: - logging.error(f"❌ 小果错误: {e}") - -async def main(): - while True: - try: - z = XiaoGuoBot() - z.xmpp.register_plugin('xep_0030') - z.xmpp.register_plugin('xep_0045') - z.xmpp.register_plugin('xep_0199') - z.xmpp.connect(host='127.0.0.1', port=5222) - await asyncio.wait_for(z.ready.wait(), timeout=30) - logging.info("小果就绪") - await asyncio.Event().wait() - except Exception as e: - logging.error(f"小果main错误: {e}") - await asyncio.sleep(3) - -if __name__ == '__main__': - asyncio.run(main()) +"""Wrapper for xmpp_agent_core.py --agent xiaoguo""" +import sys, os +sys.argv = [sys.argv[0], '--agent', 'xiaoguo'] +exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py')).read()) diff --git a/xmpp_zhiwei_bot.py b/xmpp_zhiwei_bot.py index 7ddb5f5..c537f80 100644 --- a/xmpp_zhiwei_bot.py +++ b/xmpp_zhiwei_bot.py @@ -1,192 +1,5 @@ #!/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) +"""Wrapper for xmpp_agent_core.py --agent zhiwei""" +import sys, os +sys.argv = [sys.argv[0], '--agent', 'zhiwei'] +exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py')).read())