Files
zhiwei ef93f066e3 fix: stop user-level gateway service conflicting with system-level one
Root cause: user-level hermes-gateway.service was running the same
-p default gateway as the system-level hermes-gateway@default.service,
causing the two to SIGTERM each other every ~5 minutes. This killed
in-flight Hermes API requests with 'Remote end closed connection'.

Fix: disabled the redundant user-level service.
Also: simplified webhook to just forward raw message content (no
system prompt, no ACK, no max_tokens overrides). Removed unused
_get_ack method.
2026-06-24 19:53:24 +08:00

192 lines
7.1 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 message to Hermes, get response, send back to WeChat."""
payload = json.dumps({
"model": "nova-4",
"messages": [
{"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=180)
resp_data = json.loads(resp.read())
reply = resp_data.get('choices', [{}])[0].get('message', {}).get('content', '')
log.info(f"Hermes OK, reply: {reply[:60]}")
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()