Files
MoFin/archive/old-files-20260720/xmpp_zhiwei_bot.py.bak
T

273 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
# Small HTTP server to receive proactive send requests
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
_send_queue = []
class ZhiweiSendHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
try:
data = json.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_zhiwei_http():
server = HTTPServer(('127.0.0.1', 5805), ZhiweiSendHandler)
server.timeout = 1.0
while True:
server.handle_request()
threading.Thread(target=_run_zhiwei_http, daemon=True).start()
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logging.info("🚀 知微 HTTP 桥启动于 :5805")
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._silenced = False
self._recent_sent = []
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]
if nick == 'coregroup':
nick = 'zhiwei' # 核心群用知微自己的昵称
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
# 群聊处理(jujidina
if mtype == 'groupchat':
nickname = sender.split('/')[-1] if '/' in sender else ''
# 闭嘴硬闸门
if nickname == 'hmo':
if any(kw in body for kw in ['闭嘴', '安静', '别说话', 'shut', 'silence']):
self._silenced = True
logging.info("🔇 知微闭嘴标记已设置")
return
if '@zhiwei' in body or '@知微' in body:
self._silenced = False
logging.info("🔊 知微闭嘴标记已解除")
if self._silenced:
return
if nickname == 'zhiwei':
# 自己的消息跳过,不处理
return
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=1800))
data = json.loads(result.read())
reply = data.get("choices",[{}])[0].get("message",{}).get("content","")
finish = data.get("choices",[{}])[0].get("finish_reason","")
# 处理 __SILENT__ 标记
if reply.strip().startswith('__SILENT__') or reply.strip().startswith('`__SILENT__`'):
logging.info("⏭️ 知微群决定沉默")
return
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=1800))
data = json.loads(result.read())
reply = data.get("choices",[{}])[0].get("message",{}).get("content","")
finish = data.get("choices",[{}])[0].get("finish_reason","")
# 处理 __SILENT__ 标记
if reply.strip().startswith('__SILENT__') or reply.strip().startswith('`__SILENT__`'):
logging.info("⏭️ 知微决定沉默")
return
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
# 消耗主动发送队列
async def _drain():
while True:
await asyncio.sleep(1)
while _send_queue:
room, text = _send_queue.pop(0)
try:
z.xmpp.send_message(mto=room, mbody=text, mtype='groupchat')
# 加入防回声列表
sent_norm = text.strip()[:100]
z._recent_sent.append(sent_norm)
if len(z._recent_sent) > 10:
z._recent_sent.pop(0)
logging.info(f"📤 知微主动发送到 {room}: {text[:60]}")
except Exception as e:
logging.error(f"❌ 知微主动发送失败: {e}")
asyncio.create_task(_drain())
# 保持运行,断线时自动跳出重连
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)