Compare commits
2 Commits
90ed30dd36
...
5d35d97f0a
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d35d97f0a | |||
| babbc46801 |
@@ -55,17 +55,27 @@ systemctl start xmpp-{bot}
|
||||
|
||||
```
|
||||
AgentsMeeting/
|
||||
├── README.md # 本文件
|
||||
├── gateway/ # 实际运行的代码(脚本+工具+日志)
|
||||
├── xmpp_agent_core.py # 统一 Bot 核心(所有 Agent 共享)
|
||||
│ 用法: python xmpp_agent_core.py --agent xxm|mohe|zhiwei|xiaoguo
|
||||
│ 功能: PID锁/重连/MUC/dedup/batching/coordinator协议/HTTP桥
|
||||
├── xxm_bot.py # 兼容入口 → xmpp_agent_core.py --agent xxm
|
||||
├── mohe_bot.py # 兼容入口 → xmpp_agent_core.py --agent mohe
|
||||
├── zhiwei_bot.py # 兼容入口 → xmpp_agent_core.py --agent zhiwei
|
||||
├── xiaoguo_bot.py # 兼容入口 → xmpp_agent_core.py --agent xiaoguo
|
||||
├── gateway/ # 运行时组件(脚本+工具+日志)
|
||||
│ ├── README.md # gateway 自身说明
|
||||
│ ├── scripts/ # 运行时脚本
|
||||
│ │ ├── xmpp_bot.py # XMPP Bot (HTTP桥 :5802)
|
||||
│ │ ├── chat_bridge.py # xxm LLM 桥接(SessionBridge)
|
||||
│ │ ├── session_router.py # 消息路由
|
||||
│ │ ├── wechat_agent.py # 微信桥接代理
|
||||
│ │ ├── api_proxy.py # API 反向代理 (:8787)
|
||||
│ │ ├── xmpp_watchdog.py # 进程看门狗
|
||||
│ │ ├── health_check_xxm.py# 消息流健康检查
|
||||
│ │ ├── mohe_watcher.py # 莫荷消息监控
|
||||
│ │ ├── dashboard.py # 管理门户后端
|
||||
│ │ ├── proc_guard.py # PID 锁(防重复启动)
|
||||
│ │ ├── qq_bot.py # QQ bot 桥接
|
||||
│ │ ├── vc_webhook.py # VoceChat webhook
|
||||
│ │ └── templates/
|
||||
│ │ └── dashboard.html # 管理门户前端
|
||||
│ ├── logs/ # 运行时日志
|
||||
@@ -73,37 +83,37 @@ AgentsMeeting/
|
||||
├── docs/
|
||||
│ ├── ARCHITECTURE.md # 架构设计文档
|
||||
│ ├── AUDIT.md # 稳定性审计报告
|
||||
│ ├── CLEANUP_PLAN.md # 代码清理方案
|
||||
│ ├── OBSERVER-PROTOCOL.md # 群聊观察者模式协议
|
||||
│ ├── SESSION-BRIDGE-PROTOCOL.md # 跨Session消息桥接协议
|
||||
│ ├── PRD_v0.2.md # 产品需求文档
|
||||
│ ├── DEPLOY.md # 部署指南
|
||||
│ └── OPS.md # 运维手册
|
||||
├── config/
|
||||
│ ├── .env.example # 环境变量模板
|
||||
│ └── profiles/ # 各 Agent 配置文件
|
||||
│ ├── xxm/
|
||||
│ ├── mohe/
|
||||
│ ├── xiaoguo/
|
||||
│ └── zhiwei/
|
||||
├── src/ # 重构版本(逐步迁移中)
|
||||
│ ├── shared/ # 共享库
|
||||
│ │ ├── config.py # 集中配置管理
|
||||
│ │ └── bot_base.py # Bot 基类
|
||||
│ ├── bots/ # Bot 实现
|
||||
│ ├── channels/ # 通道桥接
|
||||
│ │ ├── wechat/ # 微信桥接
|
||||
│ │ └── qq/ # QQ 桥接(规划中)
|
||||
│ └── ops/ # 运维工具
|
||||
│ ├── watchdog.py
|
||||
│ └── health_check.py
|
||||
│ ├── agents.yaml # Agent 实例注册
|
||||
│ └── .env.example # 环境变量模板
|
||||
├── configs/ # 各 Agent 配置/SOUL
|
||||
│ ├── main/
|
||||
│ ├── mohe/
|
||||
│ ├── xiaoguo/
|
||||
│ └── position-analyst/
|
||||
├── src/
|
||||
│ ├── shared/
|
||||
│ │ ├── config.py # 集中配置管理(env-var 方式)
|
||||
│ │ └── bot_base.py # Bot 基类(功能已合并进 xmpp_agent_core.py)
|
||||
│ ├── channels/
|
||||
│ │ └── qq/
|
||||
│ │ └── bridge.py # QQ 通道骨架
|
||||
│ └── ops/
|
||||
│ └── watch_group.py # 群聊监控
|
||||
├── deploy/
|
||||
│ ├── windows/
|
||||
│ │ ├── start.ps1 # 一键启动
|
||||
│ │ ├── stop.ps1 # 一键停止
|
||||
│ │ └── check.ps1 # 状态检查
|
||||
│ └── linux/
|
||||
│ ├── install.sh # 安装脚本(待 mohe)
|
||||
│ └── hermes-*.service # systemd 模板(待 mohe)
|
||||
│ ├── install.sh # 安装脚本
|
||||
│ └── hermes-*.service # systemd 模板
|
||||
└── tests/ # 测试套件
|
||||
```
|
||||
|
||||
@@ -114,8 +124,8 @@ AgentsMeeting/
|
||||
| 组件 | 方式 | 频率 |
|
||||
|------|------|------|
|
||||
| 管理门户 | dashboard.py + Web UI (:5803) | 实时轮询 5s |
|
||||
| xmpp_bot 进程 | watchdog (xmpp_watchdog.py) | 30s |
|
||||
| xmpp_bot 消息流 | health_check_xxm.py | 5min (scheduled task) |
|
||||
| xmpp Agent Bot | watchdog (xmpp_watchdog.py) | 30s |
|
||||
| xmpp Agent 消息流 | health_check_xxm.py | 5min (scheduled task) |
|
||||
| wechat_agent | 内置看门狗 | 120s |
|
||||
| 日志 | `logs/health_check.log` | 人工查看 |
|
||||
|
||||
@@ -138,7 +148,7 @@ AgentsMeeting/
|
||||
|
||||
| ID | 问题 | 平台 | 状态 |
|
||||
|----|------|------|------|
|
||||
| R01 | MUC join 超时 (conference.yoin.fun DNS) | Linux (ejabberd) | 🔴 |
|
||||
| R01 | MUC join 超时 (conference.yoin.fun DNS) | Linux (ejabberd) | 🟢 已修复(join_muc + raw presence 双保险) |
|
||||
| R02 | wechat_agent 无系统级自动恢复 | Windows | 🟡 |
|
||||
| R03 | Gateway 进程无 systemd auto-restart | Linux | 🔴 |
|
||||
| R04 | 日志无系统级轮转 | Windows + Linux | 🟡 |
|
||||
@@ -149,7 +159,7 @@ AgentsMeeting/
|
||||
## 开发流程
|
||||
|
||||
1. **架构设计** → `docs/ARCHITECTURE.md`
|
||||
2. **代码工程化** → `src/` 按特征优先组织
|
||||
2. **核心修改** → `xmpp_agent_core.py`(统一 Bot 核心,所有 Agent 共享)
|
||||
3. **部署脚本** → `deploy/` 一键启停
|
||||
4. **测试** → `tests/` 自动化测试
|
||||
5. **文档** → `docs/` 持续更新
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""XMPP Bot mohe@yoin.fun - 稳定重连版"""
|
||||
import asyncio, logging, ssl, json, urllib.request, os, time, re
|
||||
from slixmpp import ClientXMPP
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
|
||||
GATEWAY = "http://localhost:8642/v1/chat/completions"
|
||||
API_KEY = "hermes123"
|
||||
_opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
|
||||
class MoheBot(ClientXMPP):
|
||||
def __init__(self):
|
||||
super().__init__('mohe@yoin.fun', 'hermes123')
|
||||
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
|
||||
|
||||
async def on_connected(self, event):
|
||||
logging.info("🔗 TCP连接已建立")
|
||||
|
||||
async def on_bind(self, event):
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
# 加入内核组(每次重连后重新加入)
|
||||
self.plugin['xep_0045'].join_muc('coregroup@conference.yoin.fun', 'mohe')
|
||||
self._muc_joined = True
|
||||
self.ready.set()
|
||||
logging.info("✅ 莫荷 XMPP 上线")
|
||||
|
||||
async def on_disconnect(self, event):
|
||||
self.ready.clear()
|
||||
self._muc_joined = False
|
||||
logging.warning("⚠️ 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 'mohe@yoin.fun' 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)
|
||||
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", "xmpp-mohe-v2")
|
||||
|
||||
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("⏭️ 决定沉默,不发送")
|
||||
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')
|
||||
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"<message from='mohe@yoin.fun' to='{sender}' type='chat' xml:lang='en'><body>{safe}</body></message>"
|
||||
], capture_output=True, timeout=10)
|
||||
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:
|
||||
bot = MoheBot()
|
||||
bot.register_plugin('xep_0030') # Service Discovery
|
||||
bot.register_plugin('xep_0045') # MUC
|
||||
bot.register_plugin('xep_0199') # XMPP Ping(保活)
|
||||
|
||||
bot.connect(host='127.0.0.1', port=5222)
|
||||
await asyncio.wait_for(bot.ready.wait(), timeout=30)
|
||||
logging.info("莫荷 XMPP 就绪")
|
||||
retry_delay = 1 # 连接成功后重置重试间隔
|
||||
|
||||
# 保持运行,断线时自动重连
|
||||
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)
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/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"<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", "")
|
||||
# 处理 __SILENT__ 和 __REPLY__ 标记(和莫荷保持一致)
|
||||
stripped = reply.strip()
|
||||
if stripped.startswith('__SILENT__'):
|
||||
logging.info("⏭️ 小果决定沉默,不发送")
|
||||
return
|
||||
# 安全网:过滤沉默宣告类文本(防止 LLM 不按规则走)
|
||||
if re.match(r'^(保持安静|不插嘴|我沉默了|收到|明白|好的|在的?|在呢|来了|沉默|安静)([,,!!。.?\s]|$)', stripped, re.IGNORECASE):
|
||||
logging.info(f"⏭️ 小果沉默宣告被拦截: {stripped[:60]}")
|
||||
return
|
||||
reply = re.sub(r'^__REPLY__\s*', '', reply)
|
||||
if reply.strip() and finish != "silent":
|
||||
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())
|
||||
@@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""XMPP Bot zhiwei@yoin.fun - Hermes API 版(稳定重连版)"""
|
||||
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({}))
|
||||
|
||||
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 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 = True
|
||||
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_connected(self, event):
|
||||
logging.info("🔗 知微TCP连接已建立")
|
||||
|
||||
async def on_bind(self, event):
|
||||
self.xmpp.send_presence(); self.xmpp.get_roster(); self.ready.set()
|
||||
logging.info("✅ 知微上线")
|
||||
|
||||
async def on_disconnect(self, event):
|
||||
self.ready.clear()
|
||||
logging.warning("⚠️ 知微断线")
|
||||
# 不要在这里调用 self.xmpp.disconnect(),让 auto_reconnect 处理
|
||||
|
||||
async def on_msg(self, msg):
|
||||
body = msg['body']; sender = str(msg['from'])
|
||||
if not body or msg['type'] != 'chat': return
|
||||
if 'hmo@yoin.fun' in sender:
|
||||
self._call_seq += 1
|
||||
logging.info(f"📩 老爸(#{self._call_seq}): {body}")
|
||||
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=600))
|
||||
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.connect(host='127.0.0.1', port=5222)
|
||||
await asyncio.wait_for(z.ready.wait(), timeout=30); logging.info("知微就绪")
|
||||
retry_delay = 1
|
||||
|
||||
# 保持运行,断线时自动重连
|
||||
while True:
|
||||
await asyncio.sleep(15)
|
||||
if not z.xmpp.is_connected():
|
||||
logging.warning("知微连接丢失,准备重连...")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"知微main错误: {e}")
|
||||
|
||||
# 指数退避重连
|
||||
logging.info(f"⏳ 知微等待 {retry_delay} 秒后重连...")
|
||||
await asyncio.sleep(retry_delay)
|
||||
retry_delay = min(retry_delay * 2, max_delay)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,268 @@
|
||||
# AgentsMeeting 代码清理方案
|
||||
|
||||
> 目标:消除 XMPP Bot 代码碎片化,统一架构。所有 Agent 共享同一份核心代码。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状问题
|
||||
|
||||
### 1.1 四套独立 Bot 实现
|
||||
|
||||
| # | 位置 | 行数 | 状态 | 用途 | LLM 调用 |
|
||||
|---|------|-------|------|------|----------|
|
||||
| 1 | `gateway/scripts/xmpp_bot.py` | 943 | 活跃 | xxm bot | chat_bridge |
|
||||
| 2 | `xmpp_agent_core.py `(root) | 348 | 活跃 | mohe/zhiwei/xiaoguo | Hermes API |
|
||||
| 3 | `bots/xmpp_bot.py` + 姐妹文件 | 359 | 孤儿 | mohe/zhiwei/xiaoguo(旧版) | Hermes API |
|
||||
| 4 | `xmpp_bot_rest.py` | 72 | 废弃 | ejabberd REST 实验 | Hermes API |
|
||||
|
||||
**问题**:同样的 XMPP 连接、MUC join、消息处理逻辑,在 4 套代码中重复实现。修复了其中一个,其他三个还是坏的。
|
||||
|
||||
### 1.2 基础设施碎片化
|
||||
|
||||
bot_base.py(263 行)已经提炼出完善的 XMPP 基类,但没有任何 bot 实际使用它:
|
||||
|
||||
```
|
||||
bot_base.py 有:
|
||||
✓ PID 锁(proc_guard)
|
||||
✓ 自动重连 + MUC join 重试
|
||||
✓ 消息去重(dedup)
|
||||
✓ 消息合并(batching)
|
||||
✓ silence/shutup 协议
|
||||
✓ send_group / send_private
|
||||
|
||||
没有被任何 bot 使用 ✗
|
||||
```
|
||||
|
||||
### 1.3 重复/孤儿文件
|
||||
|
||||
| 文件 | 问题 |
|
||||
|------|------|
|
||||
| `xmpp_bot.py`(root) | `xmpp_agent_core.py` 的较旧副本 |
|
||||
| `xmpp_bot_rest.py` | 废弃方案 |
|
||||
| `bots/` 目录(3 文件) | 被 xmpp_agent_core.py 取代 |
|
||||
| `hermes_state.py`(root) | 孤儿,import 已断 |
|
||||
| `scripts/`(6 文件) | 一次性脚本 |
|
||||
| `src/bots/{mohe,xiaoguo,xxm,zhiwei}/` | 空目录,重构未完成 |
|
||||
| `src/channels/wechat/` | 空目录 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标架构
|
||||
|
||||
```
|
||||
AgentsMeeting/
|
||||
├── xmpp_agent_core.py ← 唯一 Bot 核心(统一架构)
|
||||
│ 支持 --agent xxm|mohe|zhiwei|xiaoguo
|
||||
│ 包含:PID锁/重连/MUC/dedup/batching/
|
||||
│ coordinator协议/HTTP桥
|
||||
│
|
||||
├── xxm_bot.py ← 兼容入口(python xxm_bot.py = python xmpp_agent_core.py --agent xxm)
|
||||
├── mohe_bot.py ← 兼容入口
|
||||
├── xiaoguo_bot.py ← 兼容入口
|
||||
├── zhiwei_bot.py ← 兼容入口
|
||||
│
|
||||
└── gateway/
|
||||
└── scripts/
|
||||
├── chat_bridge.py ← xxm LLM 桥(不变)
|
||||
├── session_router.py ← 消息路由(不变)
|
||||
└── ...(其他脚本不变)
|
||||
```
|
||||
|
||||
**核心原则**:
|
||||
- `xmpp_agent_core.py` = 唯一核心,所有 Agent 共享
|
||||
- 每个 Agent 只有 LLM 调用方式不同(在配置中定义)
|
||||
- coordinator/GRANT/REVOKE 协议只有一个实现,所有 bot 统一遵守
|
||||
- `bot_base.py` 的基础设施合并进 `xmpp_agent_core.py`
|
||||
|
||||
---
|
||||
|
||||
## 3. 迁移步骤
|
||||
|
||||
### Phase 0:确认基线(先保证现有 bot 正常工作)
|
||||
|
||||
- [ ] 确认 `gateway/scripts/xmpp_bot.py` 当前运行正常,能收发群消息
|
||||
- [ ] 确认 `xmpp_agent_core.py`(mohe)当前运行正常
|
||||
|
||||
### Phase 1:合并基础设施到 xmpp_agent_core.py
|
||||
|
||||
将 `bot_base.py` 和 `gateway/scripts/xmpp_bot.py` 中的公共功能合并进 `xmpp_agent_core.py`:
|
||||
|
||||
| 功能 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| PID 锁(proc_guard) | bot_base.py / gateway | 防重复启动 |
|
||||
| 消息去重(dedup) | bot_base.py / gateway | 防重复处理同一条消息 |
|
||||
| 消息合并(batching) | bot_base.py / gateway | 3s debounce,附近消息合并一次 LLM 调用 |
|
||||
| 自动重连 | bot_base.py / gateway | slixmpp auto_reconnect + reconnect_max_delay |
|
||||
| MUC join 双重保证 | gateway | `join_muc()` + `send_raw(presence)` |
|
||||
| MAM 启动恢复 | gateway | 启动时拉取最近 50 条历史消息补上下文 |
|
||||
| HTTP 桥丰富 API | gateway | /health, /presence, /messages, POST /send |
|
||||
| sub-agent exec | gateway | `##exec:command##` 用于工具调用 |
|
||||
| delayed reply | gateway | `##delay:N##` 延迟回复 |
|
||||
|
||||
### Phase 2:统一 coordinator 协议
|
||||
|
||||
gateway/scripts/xmpp_bot.py 已经实现了 coordinator/GRANT/REVOKE 协议。将这套实现整合进 `xmpp_agent_core.py`,确保所有 Agent 使用同一份协议代码。
|
||||
|
||||
### Phase 3:添加 xxm 作为支持 Agent
|
||||
|
||||
在 `xmpp_agent_core.py` 的 AGENTS 配置中添加 xxm:
|
||||
|
||||
```python
|
||||
AGENTS = {
|
||||
"mohe": {..., "gateway": "http://localhost:8642/v1/chat/completions"},
|
||||
"zhiwei": {..., "gateway": "http://localhost:8643/v1/chat/completions"},
|
||||
"xiaoguo":{..., "gateway": "http://localhost:8645/v1/chat/completions"},
|
||||
"xxm": {..., "bridge": "chat_bridge"}, # xxm 走本地 chat_bridge
|
||||
}
|
||||
```
|
||||
|
||||
xxm 的 LLM 调用方式不同(不走 HTTP Hermes API,走本地 `chat_bridge.py`),所以需要在核心中抽象 LLM 调用层:
|
||||
|
||||
```python
|
||||
def _call_llm(self, content: str) -> str:
|
||||
if self.cfg.get("bridge") == "chat_bridge":
|
||||
return _call_chat_bridge(content) # 本地调用
|
||||
else:
|
||||
return _call_hermes_api(content, self.cfg["gateway"]) # HTTP 调用
|
||||
```
|
||||
|
||||
### Phase 4:删除孤儿文件
|
||||
|
||||
| 文件 | 操作 |
|
||||
|------|------|
|
||||
| `xmpp_bot.py`(root) | 移到 trashbox |
|
||||
| `xmpp_bot_rest.py` | 移到 trashbox |
|
||||
| `bots/` 整个目录 | 移到 trashbox |
|
||||
| `hermes_state.py`(root) | 移到 trashbox |
|
||||
| `scripts/gen_prd.py` | 移到 trashbox |
|
||||
| `scripts/write_prd.py` | 移到 trashbox |
|
||||
| `scripts/gen_prd_v02.py` | 移到 trashbox |
|
||||
| `scripts/write_prd_v02.py` | 移到 trashbox |
|
||||
| `scripts/build_prd.py` | 移到 trashbox |
|
||||
| `scripts/test_echo.py` | 移到 trashbox |
|
||||
| `scripts/gen_b64.py` | 保留(可能是通用工具) |
|
||||
|
||||
### Phase 5:清理空目录
|
||||
|
||||
| 目录 | 操作 |
|
||||
|------|------|
|
||||
| `src/bots/mohe/` | 删除 |
|
||||
| `src/bots/xiaoguo/` | 删除 |
|
||||
| `src/bots/xxm/` | 删除 |
|
||||
| `src/bots/zhiwei/` | 删除 |
|
||||
| `src/channels/wechat/` | 删除 |
|
||||
| `gateway/assets/` | 删除 |
|
||||
|
||||
### Phase 6:整理 gateway/temp/
|
||||
|
||||
保留:PID 文件、活跃缓存(.bridge_context.jsonl、.model_cache.json)
|
||||
其余 >200 个临时文件:移到 temp/archive/ 或按需清理。
|
||||
|
||||
### Phase 7:文档更新
|
||||
|
||||
- [ ] 更新 `docs/ARCHITECTURE.md` —— 反映统一架构
|
||||
- [ ] 更新 `README.md` —— 更新项目结构描述
|
||||
- [ ] 更新 `config/agents.yaml` —— 保持准确
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心文件变更清单
|
||||
|
||||
### 修改:xmpp_agent_core.py
|
||||
|
||||
从 348 行扩展为 ~600 行,新增:
|
||||
|
||||
```
|
||||
新增功能模块:
|
||||
├── proc_guard PID 锁
|
||||
├── 消息去重(_dedup_cache)
|
||||
├── 消息合并(_batch_* 系统,3s debounce)
|
||||
├── MAM 启动恢复(_fetch_mam_history)
|
||||
├── HTTP 桥丰富版(/health, /presence, /messages, POST /send)
|
||||
├── sub-agent exec(##exec:command##)
|
||||
├── delayed reply(##delay:N##)
|
||||
├── 抽象 LLM 调用层(支持 chat_bridge + Hermes API)
|
||||
└── coordinator/GRANT/REVOKE 协议(从 gateway 版提升)
|
||||
```
|
||||
|
||||
新增 AGENTS 配置条目:
|
||||
|
||||
```python
|
||||
"xxm": {
|
||||
"jid": "xxm@yoin.fun",
|
||||
"password": "hermes123",
|
||||
"nick": "xxm",
|
||||
"name_cn": "笑笑",
|
||||
"bridge": "chat_bridge", # 使用本地 chat_bridge
|
||||
"http_port": 5802, # HTTP 桥端口
|
||||
"muc_rooms": [
|
||||
"coregroup@conference.yoin.fun",
|
||||
"jujidina@conference.yoin.fun",
|
||||
],
|
||||
"server": "192.168.1.246", # LAN 直连
|
||||
"port": 5222,
|
||||
"session_id": "ses_xxm_xmpp",
|
||||
"mention": "@xxm/@笑笑",
|
||||
}
|
||||
```
|
||||
|
||||
### 创建:xxm_bot.py
|
||||
|
||||
```
|
||||
#!/usr/bin/env python3
|
||||
"""Wrapper for xmpp_agent_core.py --agent xxm"""
|
||||
import sys, os
|
||||
sys.argv = [sys.argv[0], '--agent', 'xxm']
|
||||
exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py')).read())
|
||||
```
|
||||
|
||||
(与现有的 mohe_bot.py / zhiwei_bot.py / xiaoguo_bot.py 模式一致)
|
||||
|
||||
### 保留不变
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `gateway/scripts/chat_bridge.py` | xxm LLM 桥,不迁移 |
|
||||
| `gateway/scripts/session_router.py` | 消息路由,不迁移 |
|
||||
| `gateway/scripts/wechat_agent.py` | 微信桥接,独立组件 |
|
||||
| `gateway/scripts/qq_bot.py` | QQ bot,独立组件 |
|
||||
| `gateway/scripts/vc_webhook.py` | VoceChat webhook,独立组件 |
|
||||
| `gateway/scripts/dashboard.py` | 管理门户,独立组件 |
|
||||
| `gateway/scripts/health_check_xxm.py` | 健康检查,独立组件 |
|
||||
| `gateway/scripts/xmpp_watchdog.py` | 看门狗,独立组件 |
|
||||
| `gateway/scripts/mohe_watcher.py` | 莫荷消息监控,独立组件 |
|
||||
| `gateway/scripts/api_proxy.py` | API 代理,独立组件 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 测试计划
|
||||
|
||||
### 5.1 单元测试
|
||||
|
||||
- [ ] `python tests/test_core.py` —— bot_base 测试保持通过
|
||||
- [ ] 新增测试:合并后的 xmpp_agent_core.py 的 LLM 抽象层
|
||||
|
||||
### 5.2 集成测试
|
||||
|
||||
- [ ] `python xmpp_agent_core.py --agent xxm` 能启动、连接、加入 MUC
|
||||
- [ ] `python xmpp_agent_core.py --agent mohe` 能启动、连接、加入 MUC
|
||||
- [ ] 各 Agent 在 coregroup 中能正确响应 @mention
|
||||
- [ ] coordinator/GRANT/REVOKE 协议各 Agent 一致
|
||||
|
||||
### 5.3 部署验证
|
||||
|
||||
- [ ] `python tests/verify_deploy.py` pass
|
||||
- [ ] gateway/scripts/xmpp_bot.py → 改为调用 xmpp_agent_core.py --agent xxm
|
||||
- [ ] Linux 端 update systemd service 指向新路径
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与回滚
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|------|------|
|
||||
| 合并后 xxm bot 不工作 | Phase 0 先备份当前 `gateway/scripts/xmpp_bot.py` |
|
||||
| 协议行为不一致 | coordinator 协议从 gateway 版提取,与 xmpp_agent_core 现有逻辑逐行对比 |
|
||||
| 启动命令需要改 | 兼容入口(xxm_bot.py 等)保持 CLI 不变 |
|
||||
|
||||
**回滚方案**:Phase 1-4 每步完成后验证。出问题从 trashbox 恢复删除的文件。
|
||||
+34
-20
@@ -66,9 +66,9 @@ def get_db_handle():
|
||||
dbs = r.get("data") or []
|
||||
for db in dbs:
|
||||
dbname = db.get("databaseName", "")
|
||||
if "MSG" in dbname or "Msg" in dbname:
|
||||
db_handle_cache = db.get("handle")
|
||||
return db_handle_cache
|
||||
if dbname.startswith("MSG") and "Media" not in dbname:
|
||||
db_handle_cache = db.get("handle")
|
||||
return db_handle_cache
|
||||
return None
|
||||
|
||||
|
||||
@@ -94,24 +94,42 @@ def query_history(wxid, limit=10):
|
||||
return None
|
||||
limit_val = min(int(limit), 200)
|
||||
sql = (
|
||||
f"SELECT CreateTime, IsSender, Type, SubType, StrContent, DisplayContent "
|
||||
f"FROM MSG WHERE StrTalker='{wxid}' AND Type IN (1,49) "
|
||||
f"SELECT CreateTime, IsSender, Type, SubType, StrContent, DisplayContent, CompressContent, BytesExtra "
|
||||
f"FROM MSG WHERE StrTalker='{wxid}' AND Type IN ('1','49') "
|
||||
f"ORDER BY CreateTime DESC LIMIT {limit_val}"
|
||||
)
|
||||
r = wxpost("/api/execSql", {"dbHandle": h, "sql": sql}, timeout=15)
|
||||
data = r.get("data") or []
|
||||
if not data or len(data) < 2:
|
||||
return None
|
||||
# Skip header row, reverse to chronological order
|
||||
rows = data[1:]
|
||||
# wxhelper returns [{value: [cols]}, {value: [row1]}, ...]
|
||||
rows = [item.get("value", item) if isinstance(item, dict) else item for item in data]
|
||||
rows = rows[1:] # skip header
|
||||
rows.reverse()
|
||||
results = []
|
||||
for row in rows:
|
||||
content = (row[4] or "").strip() if len(row) > 4 else ""
|
||||
if not content and len(row) > 5:
|
||||
content = (row[5] or "").strip()
|
||||
# Type 49 (article link): extract URL from CompressContent or BytesExtra
|
||||
if not content and str(row[2]) == "49":
|
||||
try:
|
||||
import re
|
||||
# Try BytesExtra first (row[7])
|
||||
for idx in [7, 6]:
|
||||
if idx < len(row) and row[idx]:
|
||||
text = str(row[idx])
|
||||
urls = re.findall(r'https?://[^\s\x00-\x1f<>\"\']{10,}', text)
|
||||
if urls:
|
||||
content = urls[0]
|
||||
break
|
||||
except:
|
||||
pass
|
||||
if not content:
|
||||
continue
|
||||
if str(row[2]) == "49":
|
||||
content = "[文章链接]"
|
||||
else:
|
||||
continue
|
||||
results.append({
|
||||
"CreateTime": row[0],
|
||||
"IsSender": row[1],
|
||||
@@ -181,29 +199,25 @@ def get_recent_chats(limit=20):
|
||||
if not h:
|
||||
return []
|
||||
sql = (
|
||||
f"SELECT StrTalker, MAX(CreateTime) as last_time, COUNT(*) as msg_count "
|
||||
f"FROM MSG WHERE Type IN (1,49) "
|
||||
f"GROUP BY StrTalker ORDER BY last_time DESC LIMIT {min(limit, 50)}"
|
||||
f"SELECT DISTINCT StrTalker FROM MSG WHERE Type IN ('1','49') "
|
||||
f"LIMIT {min(limit, 50)}"
|
||||
)
|
||||
r = wxpost("/api/execSql", {"dbHandle": h, "sql": sql}, timeout=15)
|
||||
data = r.get("data") or []
|
||||
if not data or len(data) < 2:
|
||||
return []
|
||||
rows = [item.get("value", item) if isinstance(item, dict) else item for item in data]
|
||||
results = []
|
||||
for row in data[1:]:
|
||||
for row in rows[1:]:
|
||||
wxid = (row[0] or "").strip()
|
||||
if not wxid or wxid in ("fmessage", "weixin", "wechat", "filehelper"):
|
||||
if not wxid or wxid in ("fmessage", "weixin", "wechat", "filehelper", "medianote", "floatbottle", "qmessage"):
|
||||
continue
|
||||
if wxid.startswith("gh_"):
|
||||
continue
|
||||
ts = int(row[1]) if row[1] else 0
|
||||
count = int(row[2]) if len(row) > 2 and row[2] else 0
|
||||
results.append({
|
||||
"wxid": wxid,
|
||||
"nickname": get_nickname(wxid),
|
||||
"last_message_time": datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else None,
|
||||
"last_message_ts": ts,
|
||||
"message_count": count,
|
||||
"last_message_time": None,
|
||||
"last_message_ts": 0,
|
||||
"message_count": 0,
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ if not _lock.ok:
|
||||
BOT_WXID = "wxid_5bhmquvkbude22"
|
||||
BLOCK_WXIDS = {"fmessage", "weixin", "wechat"} # ϵͳ?˺?/???Ŷӣ----ظ?
|
||||
WX_API = "http://127.0.0.1:19088"
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
|
||||
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
|
||||
TEMP_DIR = os.path.join(PROJECT_ROOT, "temp")
|
||||
LOG_FILE = os.path.join(LOG_DIR, "wechat_agent.log")
|
||||
@@ -155,8 +156,8 @@ HERMES_KEY = "hermes123"
|
||||
SENSENOVA_KEY = "sk-aRNj3UwKSLPsDfh15QNTPwbHxahblfaO"
|
||||
SENSENOVA_URL = "https://token.sensenova.cn/v1"
|
||||
|
||||
INJECTOR = r"D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway\tools\Injector_x64.exe"
|
||||
WXHELPER_DLL = r"D:\F\NewI\opencode\daily-workspace\projects\wechat-hermes-gateway\tools\wxhelper_official_39581.dll"
|
||||
INJECTOR = os.path.join(SCRIPT_DIR, "..", "tools", "Injector_x64.exe")
|
||||
WXHELPER_DLL = os.path.join(SCRIPT_DIR, "..", "tools", "wxhelper_official_39581.dll")
|
||||
|
||||
def log(m):
|
||||
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||
@@ -569,8 +570,24 @@ def process_msg(raw_data):
|
||||
ct = d.get("content", "") or d.get("msg", "") or d.get("text", "")
|
||||
msg_type = d.get("type", 1)
|
||||
is_self = d.get("isSelf", 0) or d.get("self", 0)
|
||||
# DEBUG: capture Type 49 full XML for URL analysis
|
||||
if msg_type == 49:
|
||||
try:
|
||||
with open(LOG_DIR + "/t49_xml.txt", "a", encoding="utf-8") as _f:
|
||||
_f.write(f"\n=== {time.time()} type=49 from={fu} ===\n{ct[:10000]}\n")
|
||||
except: pass
|
||||
if "@chatroom" in fu:
|
||||
log(f"GROUP RAW DUMP: keys={list(d.keys())} ct_len={len(ct)} ct[:100]={ct[:100]}")
|
||||
# DEBUG: capture full raw data for quote analysis
|
||||
try:
|
||||
with open(LOG_DIR + "/group_raw.jsonl", "a", encoding="utf-8") as _f:
|
||||
_f.write(json.dumps({k: str(v)[:2000] for k, v in d.items()}, ensure_ascii=False) + "\n")
|
||||
except: pass
|
||||
# DEBUG: capture all raw msgs for field analysis
|
||||
try:
|
||||
with open(LOG_DIR + "/all_raw.jsonl", "a", encoding="utf-8") as _f:
|
||||
_f.write(json.dumps({k: str(v)[:500] for k, v in d.items()}, ensure_ascii=False) + "\n")
|
||||
except: pass
|
||||
if not fu or not ct or fu == BOT_WXID or fu in BLOCK_WXIDS or fu.startswith("gh_") or is_self:
|
||||
log(f"SKIP: fu={fu} self={is_self}")
|
||||
return
|
||||
@@ -608,6 +625,64 @@ def process_msg(raw_data):
|
||||
else:
|
||||
log(f"-> {fu}: skip (blank image response)")
|
||||
return
|
||||
# Type 49 (forwarded article) - extract URL and process via article_processor
|
||||
if msg_type == 49 and ct.strip().startswith("<?xml"):
|
||||
try:
|
||||
import re
|
||||
# Try <url> first, then <shareUrlOriginal>, then <shareUrlOpen>
|
||||
urls = re.findall(r'<url>(https?://mp\.weixin\.qq\.com[^<]+)</url>', ct)
|
||||
if not urls:
|
||||
urls = re.findall(r'<shareUrlOriginal>(https?://mp\.weixin\.qq\.com[^<]+)</shareUrlOriginal>', ct)
|
||||
if not urls:
|
||||
urls = re.findall(r'<shareUrlOpen>(https?://mp\.weixin\.qq\.com[^<]+)</shareUrlOpen>', ct)
|
||||
url = urls[0] if urls else None
|
||||
# Extract title from XML
|
||||
titles = re.findall(r'<title>(.*?)</title>', ct)
|
||||
title = titles[0] if titles else ""
|
||||
# Extract description
|
||||
descs = re.findall(r'<des>(.*?)</des>', ct)
|
||||
desc = descs[0] if descs else ""
|
||||
|
||||
if url:
|
||||
log(f"ARTICLE URL: {url}")
|
||||
# Call article_processor on localhost
|
||||
import urllib.request as ur
|
||||
req = ur.Request("http://127.0.0.1:5810/process",
|
||||
data=json.dumps({"url": url}).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with ur.urlopen(req, timeout=180) as resp:
|
||||
result = json.loads(resp.read().decode("utf-8"))
|
||||
if result.get("status") == "ok":
|
||||
content = result.get("content", "")[:3000]
|
||||
title = result.get("title", "")
|
||||
images = result.get("images_ocr", 0)
|
||||
enriched = f"[老莫转发了一篇文章{(chr(10)+'标题: '+title) if title else ''},{images}张图片已OCR]\n\n{content}"
|
||||
log(f"ARTICLE processed: {len(content)} chars")
|
||||
reply = call_hermes(fu, enriched)
|
||||
if reply and reply.strip():
|
||||
log(f"-> {fu}: {reply[:50]}")
|
||||
send_wx(fu, reply.strip())
|
||||
return
|
||||
else:
|
||||
log(f"ARTICLE process failed: {result.get('error','')[:100]}")
|
||||
# Fallback: send title + description
|
||||
fallback = f"[老莫转发了一篇文章]{(chr(10)+'标题: '+title) if title else ''}{(chr(10)+'摘要: '+desc[:200]) if desc else ''}\n(全文抓取失败: {result.get('error','')[:60]})"
|
||||
reply = call_hermes(fu, fallback)
|
||||
if reply and reply.strip():
|
||||
send_wx(fu, reply.strip())
|
||||
return
|
||||
else:
|
||||
# No URL found, send title + description
|
||||
if title:
|
||||
log(f"ARTICLE: no URL, sending title+desc")
|
||||
fallback = f"[老莫转发了一篇文章]{(chr(10)+'标题: '+title) if title else ''}{(chr(10)+'摘要: '+desc[:200]) if desc else ''}"
|
||||
reply = call_hermes(fu, fallback)
|
||||
if reply and reply.strip():
|
||||
send_wx(fu, reply.strip())
|
||||
return
|
||||
except Exception as e:
|
||||
log(f"ARTICLE handler error: {e}")
|
||||
# Fall through to text handler
|
||||
# Text - prepend sender wxid+name so Hermes knows who's talking
|
||||
sender_name = get_nickname(fu)
|
||||
chat_type = "Group" if "@chatroom" in fu else "Private"
|
||||
|
||||
+18
-19
@@ -26,8 +26,8 @@ if not _lock.ok:
|
||||
# ── Config ──
|
||||
JID = "xxm@yoin.fun"
|
||||
PASSWORD = "hermes123"
|
||||
SERVER = "xmpp.yoin.fun"
|
||||
PORT = 3021
|
||||
SERVER = "192.168.1.246"
|
||||
PORT = 5222
|
||||
ATTACH_SESSION = "ses_xxm_xmpp"
|
||||
MUC_ROOMS = [
|
||||
"coregroup@conference.yoin.fun", # core group chat
|
||||
@@ -696,23 +696,22 @@ if __name__ == "__main__":
|
||||
bot_nick = JID.split("@")[0]
|
||||
async def _join_silent():
|
||||
for room_jid in MUC_ROOMS:
|
||||
for attempt in range(3):
|
||||
try:
|
||||
# Use join_muc_wait to ensure room join completes
|
||||
await self.plugin['xep_0045'].join_muc_wait(room_jid, bot_nick, timeout=60)
|
||||
log(f"Joined {room_jid} (silent)")
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
log(f"MUC join timeout ({attempt+1}/3) for {room_jid}")
|
||||
if attempt == 2:
|
||||
log(f"MUC setup failed for {room_jid} after 3 attempts")
|
||||
await asyncio.sleep(5)
|
||||
else:
|
||||
await asyncio.sleep(3)
|
||||
except Exception as e:
|
||||
log(f"MUC setup failed for {room_jid}: {e} (type={type(e).__name__})")
|
||||
await asyncio.sleep(5)
|
||||
break
|
||||
nick = bot_nick
|
||||
try:
|
||||
# Use join_muc (non-waiting) to register plugin state
|
||||
self.plugin['xep_0045'].join_muc(room_jid, nick)
|
||||
# Also send raw presence as backup
|
||||
presence = (
|
||||
f"<presence to='{room_jid}/{nick}'>"
|
||||
f"<x xmlns='http://jabber.org/protocol/muc'>"
|
||||
f"<history maxstanzas='0'/>"
|
||||
f"</x></presence>"
|
||||
)
|
||||
self.send_raw(presence)
|
||||
log(f"Joined {room_jid} (async)")
|
||||
except Exception as e:
|
||||
log(f"MUC join failed for {room_jid}: {type(e).__name__}: {e}")
|
||||
await asyncio.sleep(2)
|
||||
# After joining, query MAM for recent history
|
||||
await asyncio.sleep(3) # wait for MUC join to propagate
|
||||
await _fetch_mam_history()
|
||||
|
||||
-4372
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
path = r"D:\F\NewI\opencode\daily-workspace\projects\AgentsMeeting\docs\PRD.md"
|
||||
lines = []
|
||||
lines.append("# AgentsMeeting -- PRD v0.1")
|
||||
lines.append("")
|
||||
lines.append("> \u7248\u672c: \u521d\u7a3f ^| \u5ba2\u6237: hmo (\u8001\u83ab) ^| PM: mohe (\u83ab\u8377) ^| \u7814\u53d1: xxm (\u5c0f\u5c0f\u83ab)")
|
||||
@@ -1 +0,0 @@
|
||||
import os; open(os.path.join(r"D:\F\NewI\opencode\daily-workspace\projects\AgentsMeeting\docs","PRD.md"),"w",encoding="utf-8").write("# AgentsMeeting - PRD v0.1\n\nOK")
|
||||
@@ -1,2 +0,0 @@
|
||||
import os
|
||||
print("ok")
|
||||
@@ -1 +0,0 @@
|
||||
print("test123")
|
||||
@@ -1 +0,0 @@
|
||||
print("ok")
|
||||
@@ -1,2 +0,0 @@
|
||||
import os,sys
|
||||
open("D:/F/NewI/opencode/daily-workspace/projects/AgentsMeeting/docs/PRD_v0.2.md","w",encoding="utf-8").write("test ok\n")
|
||||
+831
-322
File diff suppressed because it is too large
Load Diff
-269
@@ -1,269 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""XMPP Bot mohe@yoin.fun - 通过 ejabberd REST API 实现"""
|
||||
import asyncio, logging, ssl, json, urllib.request, os, time
|
||||
import subprocess, threading
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
|
||||
GATEWAY = "http://localhost:8642/v1/chat/completions"
|
||||
API_KEY = "hermes123"
|
||||
EJB_ADMIN = "admin@localhost"
|
||||
EJB_PASS = "hermes123"
|
||||
_opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
|
||||
LAST_SEQ = 0
|
||||
|
||||
def call_api(content, sender, seq):
|
||||
"""同步调 Hermes API 并回复"""
|
||||
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", "xmpp-mohe")
|
||||
|
||||
result = _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", "")
|
||||
|
||||
global LAST_SEQ
|
||||
if seq < LAST_SEQ:
|
||||
logging.info(f"⏭️ 跳过过期 seq={seq}")
|
||||
return
|
||||
|
||||
if reply.strip() and finish != "silent":
|
||||
# 通过 ejabberdctl 发送回复
|
||||
subprocess.run([
|
||||
"docker", "exec", "ejabberd", "ejabberdctl", "send_stanza",
|
||||
"mohe@yoin.fun", sender,
|
||||
f"<message from='mohe@yoin.fun' to='{sender}' type='chat' xml:lang='en'><body>{reply}</body></message>"
|
||||
], capture_output=True, timeout=30)
|
||||
logging.info(f"✅ 回复: {reply[:80]}")
|
||||
except Exception as e:
|
||||
logging.error(f"❌ 错误: {e}")
|
||||
|
||||
def poll_messages():
|
||||
"""轮询 ejabberd 离线消息"""
|
||||
global LAST_SEQ
|
||||
while True:
|
||||
try:
|
||||
# 用 ejabberdctl 获取 mohe 的离线消息
|
||||
result = subprocess.run([
|
||||
"docker", "exec", "ejabberd", "ejabberdctl", "get_offline_count", "mohe", "yoin.fun"
|
||||
], capture_output=True, text=True, timeout=10)
|
||||
count = int(result.stdout.strip())
|
||||
|
||||
if count > 0:
|
||||
# 获取消息内容并处理
|
||||
result2 = subprocess.run([
|
||||
"docker", "exec", "ejabberd", "ejabberdctl", "get_offline_messages", "mohe", "yoin.fun"
|
||||
], capture_output=True, text=True, timeout=10)
|
||||
# 解析消息并处理(简化处理)
|
||||
except:
|
||||
pass
|
||||
time.sleep(5)
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 实际上需要通过 XMPP 连接或 BOSH/WS
|
||||
# 这个方案太复杂,直接换个思路:让 ejabberd → webhook → 处理 → reponse
|
||||
print("需要更简单的方法")
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
"""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())
|
||||
exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py'), encoding='utf-8').read())
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
"""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())
|
||||
exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py'), encoding='utf-8').read())
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
"""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())
|
||||
exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py'), encoding='utf-8').read())
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Wrapper for xmpp_agent_core.py --agent xxm"""
|
||||
import sys, os
|
||||
sys.argv = [sys.argv[0], '--agent', 'xxm']
|
||||
exec(open(os.path.join(os.path.dirname(__file__), 'xmpp_agent_core.py'), encoding='utf-8').read())
|
||||
Reference in New Issue
Block a user