feat: WeChat Linux bot via docker-wechatbot-webhook
- Docker container with auto-restart - systemd webhook receiver on :5804 - Full send/receive loop: WeChat ↔ Docker ↔ Hermes - Fixed login token for persistence - Firewall rules for container-host communication
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""WeChat webhook receiver v2 - receives messages from docker-wechatbot-webhook."""
|
||||
|
||||
import os, sys, json, logging, threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
HERMES_API = "http://192.168.1.246:8642/v1/chat/completions"
|
||||
HERMES_KEY = "hermes123"
|
||||
PORT = 5804
|
||||
|
||||
WECHAT_BOT_TOKEN = os.environ.get("WECHAT_BOT_TOKEN", "mowechat_fixed_token_001")
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler("/home/hmo/projects/AgentsMeeting/gateway/linux/logs/webhook.log"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
log = logging.getLogger("wc-webhook")
|
||||
|
||||
|
||||
class WebhookHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
content_type = self.headers.get('Content-Type', '')
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
|
||||
# Read the body
|
||||
body = self.rfile.read(content_length)
|
||||
|
||||
# Parse multipart/form-data
|
||||
msg_type = 'unknown'
|
||||
content = ''
|
||||
source_raw = '{}'
|
||||
is_system = '0'
|
||||
|
||||
if 'multipart/form-data' in content_type:
|
||||
# Manual multipart parsing
|
||||
boundary = content_type.split('boundary=')[1].strip()
|
||||
if boundary.startswith('"') and boundary.endswith('"'):
|
||||
boundary = boundary[1:-1]
|
||||
|
||||
parts = body.split(b'--' + boundary.encode())
|
||||
for part in parts:
|
||||
if b'Content-Disposition' not in part:
|
||||
continue
|
||||
|
||||
# Parse headers
|
||||
header_end = part.find(b'\r\n\r\n')
|
||||
if header_end < 0:
|
||||
continue
|
||||
part_headers = part[:header_end].decode('utf-8', errors='replace')
|
||||
part_body = part[header_end + 4:]
|
||||
|
||||
# Get field name
|
||||
name_start = part_headers.find('name="')
|
||||
if name_start < 0:
|
||||
continue
|
||||
name_start += 6
|
||||
name_end = part_headers.find('"', name_start)
|
||||
field_name = part_headers[name_start:name_end]
|
||||
|
||||
# Trim trailing \r\n--
|
||||
if part_body.endswith(b'\r\n'):
|
||||
part_body = part_body[:-2]
|
||||
if part_body.endswith(b'--'):
|
||||
part_body = part_body[:-2]
|
||||
if part_body.endswith(b'\r\n'):
|
||||
part_body = part_body[:-2]
|
||||
|
||||
if field_name == 'type':
|
||||
msg_type = part_body.decode('utf-8', errors='replace')
|
||||
elif field_name == 'content':
|
||||
content = part_body.decode('utf-8', errors='replace')
|
||||
elif field_name == 'source':
|
||||
source_raw = part_body.decode('utf-8', errors='replace')
|
||||
elif field_name == 'isSystemEvent':
|
||||
is_system = part_body.decode('utf-8', errors='replace')
|
||||
else:
|
||||
# Try as regular form or JSON
|
||||
try:
|
||||
data = json.loads(body)
|
||||
msg_type = data.get('type', 'unknown')
|
||||
content = data.get('content', '')
|
||||
source_raw = data.get('source', '{}')
|
||||
except:
|
||||
pass
|
||||
|
||||
# Parse source
|
||||
try:
|
||||
source = json.loads(source_raw) if isinstance(source_raw, str) else source_raw
|
||||
except:
|
||||
source = {}
|
||||
|
||||
# Extract sender info
|
||||
sender_name = "unknown"
|
||||
sender_id = "unknown"
|
||||
if isinstance(source, dict):
|
||||
from_data = source.get('from', {})
|
||||
if isinstance(from_data, dict):
|
||||
payload = from_data.get('payload', {})
|
||||
sender_name = payload.get('name', 'unknown')
|
||||
sender_id = payload.get('id', 'unknown')
|
||||
|
||||
# Skip system events
|
||||
if is_system == '1':
|
||||
log.info(f"System event: {msg_type}")
|
||||
self._respond(200, {"status": "ok"})
|
||||
return
|
||||
|
||||
log.info(f"From: {sender_name} ({sender_id}), Type: {msg_type}")
|
||||
|
||||
if msg_type == 'text':
|
||||
log.info(f"Text: {content[:300]}")
|
||||
self._forward_to_hermes(sender_name, sender_id, content)
|
||||
elif msg_type == 'urlLink':
|
||||
log.info(f"URL: {content[:200]}")
|
||||
self._forward_to_hermes(sender_name, sender_id, f"[分享链接] {content}")
|
||||
elif msg_type == 'file':
|
||||
log.info(f"File received, length={len(body)}")
|
||||
else:
|
||||
log.info(f"Other: {msg_type}")
|
||||
|
||||
self._respond(200, {"status": "ok"})
|
||||
|
||||
def _forward_to_hermes(self, sender, sender_id, text):
|
||||
"""Forward to Hermes and send reply back to WeChat."""
|
||||
payload = json.dumps({
|
||||
"model": "nova-4",
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是莫荷微信Bot。回复简洁,不要废话。"},
|
||||
{"role": "user", "content": f"[微信消息] 来自 {sender}({sender_id}): {text}"}
|
||||
]
|
||||
}).encode()
|
||||
|
||||
def do_forward():
|
||||
try:
|
||||
import urllib.request as ureq
|
||||
handler = ureq.ProxyHandler({})
|
||||
opener = ureq.build_opener(handler)
|
||||
req = ureq.Request(HERMES_API, data=payload,
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {HERMES_KEY}"})
|
||||
resp = opener.open(req, timeout=30)
|
||||
resp_data = json.loads(resp.read())
|
||||
reply = resp_data.get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
log.info(f"Hermes OK, reply: {reply[:60]}")
|
||||
|
||||
# Send reply back via WeChat API
|
||||
if reply and sender:
|
||||
self._send_wechat(sender, reply)
|
||||
except Exception as e:
|
||||
log.error(f"Hermes error: {e}")
|
||||
|
||||
threading.Thread(target=do_forward, daemon=True).start()
|
||||
|
||||
def _send_wechat(self, to_name, text):
|
||||
"""Send message back to WeChat user via bot API."""
|
||||
import urllib.request as ureq
|
||||
token = WECHAT_BOT_TOKEN
|
||||
api = f"http://localhost:3001/webhook/msg/v2?token={token}"
|
||||
data = json.dumps({"to": to_name, "data": {"content": text}}).encode()
|
||||
try:
|
||||
handler = ureq.ProxyHandler({})
|
||||
opener = ureq.build_opener(handler)
|
||||
req = ureq.Request(api, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
resp = opener.open(req, timeout=10)
|
||||
log.info(f"WeChat send OK: {resp.status}")
|
||||
except Exception as e:
|
||||
log.error(f"WeChat send error: {e}")
|
||||
|
||||
def _respond(self, code, data):
|
||||
self.send_response(code)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode())
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
server = HTTPServer(('0.0.0.0', PORT), WebhookHandler)
|
||||
log.info(f"Webhook receiver on :{PORT}")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
server.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user