f1630ebb03
- 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
194 lines
7.2 KiB
Python
194 lines
7.2 KiB
Python
#!/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()
|