unify xmpp bots: single xmpp_agent_core.py + --agent param wrappers
This commit is contained in:
@@ -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"<message from='{AGENT_JID}' to='{sender}' type='chat' xml:lang='en'><body>{safe}</body></message>"
|
||||||
|
], 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
|
||||||
+158
-32
@@ -1,16 +1,97 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""XMPP Bot mohe@yoin.fun - 稳定重连版"""
|
"""XMPP Bot - 统一版,支持 --agent mohe|zhiwei|xiao 参数"""
|
||||||
import asyncio, logging, ssl, json, urllib.request, os, time, re
|
import asyncio, logging, ssl, json, urllib.request, os, time, sys
|
||||||
from slixmpp import ClientXMPP
|
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')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
|
||||||
GATEWAY = "http://localhost:8642/v1/chat/completions"
|
GATEWAY = cfg["gateway"]
|
||||||
API_KEY = "hermes123"
|
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({}))
|
_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):
|
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('session_bind', self.on_bind)
|
||||||
self.add_event_handler('message', self.on_msg)
|
self.add_event_handler('message', self.on_msg)
|
||||||
self.add_event_handler('disconnected', self.on_disconnect)
|
self.add_event_handler('disconnected', self.on_disconnect)
|
||||||
@@ -22,23 +103,27 @@ class MoheBot(ClientXMPP):
|
|||||||
self.ready = asyncio.Event()
|
self.ready = asyncio.Event()
|
||||||
self._call_seq = 0
|
self._call_seq = 0
|
||||||
self._muc_joined = False
|
self._muc_joined = False
|
||||||
|
self._recent_sent = []
|
||||||
|
|
||||||
async def on_connected(self, event):
|
async def on_connected(self, event):
|
||||||
logging.info("🔗 TCP连接已建立")
|
logging.info(f"🔗 {AGENT_NAME} TCP连接已建立")
|
||||||
|
|
||||||
async def on_bind(self, event):
|
async def on_bind(self, event):
|
||||||
self.send_presence()
|
self.send_presence()
|
||||||
self.get_roster()
|
self.get_roster()
|
||||||
# 加入内核组(每次重连后重新加入)
|
try:
|
||||||
self.plugin['xep_0045'].join_muc('coregroup@conference.yoin.fun', 'mohe')
|
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._muc_joined = True
|
||||||
self.ready.set()
|
self.ready.set()
|
||||||
logging.info("✅ 莫荷 XMPP 上线")
|
logging.info(f"✅ {AGENT_NAME} XMPP 上线")
|
||||||
|
|
||||||
async def on_disconnect(self, event):
|
async def on_disconnect(self, event):
|
||||||
self.ready.clear()
|
self.ready.clear()
|
||||||
self._muc_joined = False
|
self._muc_joined = False
|
||||||
logging.warning("⚠️ XMPP 断线")
|
logging.warning(f"⚠️ {AGENT_NAME} XMPP 断线")
|
||||||
|
|
||||||
async def on_msg(self, msg):
|
async def on_msg(self, msg):
|
||||||
body = msg['body']
|
body = msg['body']
|
||||||
@@ -47,13 +132,36 @@ class MoheBot(ClientXMPP):
|
|||||||
if not body:
|
if not body:
|
||||||
return
|
return
|
||||||
if msg_type == 'groupchat':
|
if msg_type == 'groupchat':
|
||||||
if 'mohe@yoin.fun' in sender:
|
if AGENT_JID in sender:
|
||||||
return
|
return
|
||||||
nickname = sender.split('/')[-1] if '/' in sender else ''
|
nickname = sender.split('/')[-1] if '/' in sender else ''
|
||||||
if nickname in ('hmo', 'xxm'):
|
# 自己的消息跳过(通过昵称)
|
||||||
|
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]}")
|
logging.info(f"📩 群消息 [{sender}]: {body[:100]}")
|
||||||
room = sender.split('/')[0]
|
room = sender.split('/')[0]
|
||||||
ctx_body = f"[核心群 {room}] {nickname} 说: {body}"
|
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)
|
await self.call_hermes(ctx_body, room, is_group=True)
|
||||||
return
|
return
|
||||||
if msg_type == 'chat' and 'hmo@yoin.fun' in sender:
|
if msg_type == 'chat' and 'hmo@yoin.fun' in sender:
|
||||||
@@ -71,7 +179,7 @@ class MoheBot(ClientXMPP):
|
|||||||
req = urllib.request.Request(GATEWAY, data=payload, method="POST")
|
req = urllib.request.Request(GATEWAY, data=payload, method="POST")
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
req.add_header("Authorization", f"Bearer {API_KEY}")
|
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()
|
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=600))
|
||||||
@@ -81,45 +189,64 @@ class MoheBot(ClientXMPP):
|
|||||||
|
|
||||||
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", "")
|
||||||
# 处理 __SILENT__ 和 __REPLY__ 标记
|
reply_stripped = reply.strip()
|
||||||
if reply.strip().startswith('__SILENT__'):
|
if reply_stripped.startswith('__SILENT__') or reply_stripped.startswith('`__SILENT__`'):
|
||||||
logging.info("⏭️ 决定沉默,不发送")
|
logging.info(f"⏭️ {AGENT_NAME} 决定沉默,不发送")
|
||||||
return
|
return
|
||||||
reply = re.sub(r'^__REPLY__\s*', '', reply)
|
|
||||||
finish = data.get("choices", [{}])[0].get("finish_reason", "")
|
finish = data.get("choices", [{}])[0].get("finish_reason", "")
|
||||||
|
|
||||||
if reply.strip() and finish != "silent":
|
if reply.strip() and finish != "silent":
|
||||||
if msg_type == 'groupchat':
|
if msg_type == 'groupchat':
|
||||||
self.send_message(mto=sender, mbody=reply, mtype='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:
|
else:
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
from xml.sax.saxutils import escape
|
from xml.sax.saxutils import escape
|
||||||
safe = escape(reply)
|
safe = escape(reply)
|
||||||
sp.run([
|
sp.run([
|
||||||
"docker", "exec", "ejabberd", "ejabberdctl", "send_stanza",
|
"docker", "exec", "ejabberd", "ejabberdctl", "send_stanza",
|
||||||
"mohe@yoin.fun", str(sender),
|
AGENT_JID, str(sender),
|
||||||
f"<message from='mohe@yoin.fun' to='{sender}' type='chat' xml:lang='en'><body>{safe}</body></message>"
|
f"<message from='{AGENT_JID}' to='{sender}' type='chat' xml:lang='en'><body>{safe}</body></message>"
|
||||||
], capture_output=True, timeout=10)
|
], capture_output=True, timeout=10)
|
||||||
logging.info(f"✅ 回复: {reply[:80]}")
|
logging.info(f"✅ {AGENT_NAME} 回复: {reply[:80]}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"❌ 错误: {e}")
|
logging.error(f"❌ {AGENT_NAME} 错误: {e}")
|
||||||
|
|
||||||
|
# ── 主入口 ───────────────────────────────────────────────
|
||||||
async def main():
|
async def main():
|
||||||
retry_delay = 1 # 初始重试间隔(秒)
|
retry_delay = 1
|
||||||
max_delay = 60 # 最大重试间隔
|
max_delay = 60
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
bot = MoheBot()
|
bot = AgentBot()
|
||||||
bot.register_plugin('xep_0030') # Service Discovery
|
bot.register_plugin('xep_0030')
|
||||||
bot.register_plugin('xep_0045') # MUC
|
bot.register_plugin('xep_0045')
|
||||||
bot.register_plugin('xep_0199') # XMPP Ping(保活)
|
bot.register_plugin('xep_0199')
|
||||||
|
|
||||||
bot.connect(host='127.0.0.1', port=5222)
|
bot.connect(host='127.0.0.1', port=5222)
|
||||||
await asyncio.wait_for(bot.ready.wait(), timeout=30)
|
await asyncio.wait_for(bot.ready.wait(), timeout=30)
|
||||||
logging.info("莫荷 XMPP 就绪")
|
logging.info(f"{AGENT_NAME} XMPP 就绪")
|
||||||
retry_delay = 1 # 连接成功后重置重试间隔
|
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:
|
while True:
|
||||||
await asyncio.sleep(15)
|
await asyncio.sleep(15)
|
||||||
if not bot.is_connected():
|
if not bot.is_connected():
|
||||||
@@ -131,7 +258,6 @@ async def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"❌ 主循环错误: {e}")
|
logging.error(f"❌ 主循环错误: {e}")
|
||||||
|
|
||||||
# 指数退避重连:1s → 2s → 4s → 8s → ... → 60s max
|
|
||||||
logging.info(f"⏳ 等待 {retry_delay} 秒后重连...")
|
logging.info(f"⏳ 等待 {retry_delay} 秒后重连...")
|
||||||
await asyncio.sleep(retry_delay)
|
await asyncio.sleep(retry_delay)
|
||||||
retry_delay = min(retry_delay * 2, max_delay)
|
retry_delay = min(retry_delay * 2, max_delay)
|
||||||
|
|||||||
@@ -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())
|
||||||
+4
-134
@@ -1,135 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""XMPP Bot xiaoguo@yoin.fun - 跑在 Linux 上"""
|
"""Wrapper for xmpp_agent_core.py --agent xiaoguo"""
|
||||||
import asyncio, logging, ssl, json, urllib.request, subprocess, re
|
import sys, os
|
||||||
from xml.sax.saxutils import escape
|
sys.argv = [sys.argv[0], '--agent', 'xiaoguo']
|
||||||
|
exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py')).read())
|
||||||
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"<message from='{from_jid}' to='{to_jid}' type='chat' xml:lang='en'><body>{safe}</body></message>"
|
|
||||||
], 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())
|
|
||||||
|
|||||||
+4
-191
@@ -1,192 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""XMPP Bot zhiwei@yoin.fun - Hermes API 版(修复版:ejabberd 26.4 兼容 + 稳定重连 + 群聊配置化)"""
|
"""Wrapper for xmpp_agent_core.py --agent zhiwei"""
|
||||||
import asyncio, logging, ssl, json, urllib.request, os, subprocess, time
|
import sys, os
|
||||||
from xml.sax.saxutils import escape
|
sys.argv = [sys.argv[0], '--agent', 'zhiwei']
|
||||||
|
exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py')).read())
|
||||||
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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user