zhiwei bot: 添加MUC群聊支持(配置化房间列表 + 群消息收发 + 稳定重连)
改动内容: - 从配置文件 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 文件防多进程
This commit is contained in:
+91
-9
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
import asyncio, logging, ssl, json, urllib.request, os, subprocess, time
|
||||||
from xml.sax.saxutils import escape
|
from xml.sax.saxutils import escape
|
||||||
|
|
||||||
@@ -8,6 +8,16 @@ GATEWAY = "http://localhost:8643/v1/chat/completions"
|
|||||||
API_KEY = "hermes123"
|
API_KEY = "hermes123"
|
||||||
_opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
_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):
|
def send(from_jid, to_jid, body):
|
||||||
safe = escape(body)
|
safe = escape(body)
|
||||||
subprocess.run(["docker","exec","ejabberd","ejabberdctl","send_message","chat",
|
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('message', self.on_msg)
|
||||||
self.xmpp.add_event_handler('disconnected', self.on_disconnect)
|
self.xmpp.add_event_handler('disconnected', self.on_disconnect)
|
||||||
self.xmpp.add_event_handler('connected', self.on_connected)
|
self.xmpp.add_event_handler('connected', self.on_connected)
|
||||||
# 启用 slixmpp 内置自动重连
|
# 不用 slixmpp 内置自动重连(不可靠),用手动重连
|
||||||
self.xmpp.auto_reconnect = True
|
self.xmpp.auto_reconnect = False
|
||||||
ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
|
ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
|
||||||
self.xmpp.ssl_context = ctx
|
self.xmpp.ssl_context = ctx
|
||||||
self.ready = asyncio.Event()
|
self.ready = asyncio.Event()
|
||||||
@@ -38,7 +48,16 @@ class ZhiweiBot:
|
|||||||
logging.info("🔗 知微TCP连接已建立")
|
logging.info("🔗 知微TCP连接已建立")
|
||||||
|
|
||||||
async def on_bind(self, event):
|
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("✅ 知微上线")
|
logging.info("✅ 知微上线")
|
||||||
|
|
||||||
async def on_disconnect(self, event):
|
async def on_disconnect(self, event):
|
||||||
@@ -46,11 +65,45 @@ class ZhiweiBot:
|
|||||||
logging.warning("⚠️ 知微断线")
|
logging.warning("⚠️ 知微断线")
|
||||||
|
|
||||||
async def on_msg(self, msg):
|
async def on_msg(self, msg):
|
||||||
body = msg['body']; sender = str(msg['from'])
|
body = msg['body']; sender = str(msg['from']); mtype = msg['type']
|
||||||
if not body or msg['type'] != 'chat': return
|
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:
|
if 'hmo@yoin.fun' in sender:
|
||||||
self._call_seq += 1
|
self._call_seq += 1
|
||||||
logging.info(f"📩 老爸(#{self._call_seq}): {body}")
|
logging.info(f"📩 老爸(#{self._call_seq}): {body}")
|
||||||
|
# 先秒回确认消息,让老爸知道bot活着
|
||||||
|
send("zhiwei@yoin.fun", sender, "收到,正在思考...")
|
||||||
try:
|
try:
|
||||||
payload = json.dumps({
|
payload = json.dumps({
|
||||||
"model":"hermes-agent",
|
"model":"hermes-agent",
|
||||||
@@ -61,7 +114,7 @@ class ZhiweiBot:
|
|||||||
req.add_header("Authorization",f"Bearer {API_KEY}")
|
req.add_header("Authorization",f"Bearer {API_KEY}")
|
||||||
req.add_header("X-Hermes-Session-Id","xmpp-zhiwei")
|
req.add_header("X-Hermes-Session-Id","xmpp-zhiwei")
|
||||||
loop = asyncio.get_event_loop()
|
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())
|
data = json.loads(result.read())
|
||||||
reply = data.get("choices",[{}])[0].get("message",{}).get("content","")
|
reply = data.get("choices",[{}])[0].get("message",{}).get("content","")
|
||||||
finish = data.get("choices",[{}])[0].get("finish_reason","")
|
finish = data.get("choices",[{}])[0].get("finish_reason","")
|
||||||
@@ -78,14 +131,22 @@ async def main():
|
|||||||
try:
|
try:
|
||||||
z = ZhiweiBot()
|
z = ZhiweiBot()
|
||||||
z.xmpp.register_plugin('xep_0030'); z.xmpp.register_plugin('xep_0199')
|
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)
|
z.xmpp.connect(host='127.0.0.1', port=5222)
|
||||||
await asyncio.wait_for(z.ready.wait(), timeout=30)
|
await asyncio.wait_for(z.ready.wait(), timeout=30)
|
||||||
logging.info("知微就绪")
|
logging.info("知微就绪")
|
||||||
retry_delay = 1
|
retry_delay = 1
|
||||||
|
|
||||||
# 保持运行 — slixmpp 内置 auto_reconnect 会自动处理断线重连
|
# 保持运行,断线时自动跳出重连
|
||||||
while not z._stopped:
|
while not z._stopped:
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(3)
|
||||||
|
if not z.xmpp.is_connected():
|
||||||
|
logging.warning("知微连接丢失,重连中...")
|
||||||
|
break
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logging.error("⏰ 知微连接超时")
|
logging.error("⏰ 知微连接超时")
|
||||||
@@ -104,7 +165,28 @@ async def main():
|
|||||||
retry_delay = min(retry_delay * 2, max_delay)
|
retry_delay = min(retry_delay * 2, max_delay)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
|
if os.path.exists(PIDFILE):
|
||||||
|
os.remove(PIDFILE)
|
||||||
|
|||||||
Reference in New Issue
Block a user