feat(wechat): widen 105 egress proxy to /cgi-bin/* and add egress fetch helper

The proxy now forwards any api.weixin.qq.com/cgi-bin/* path (drafts, material
upload, etc.), binds to the 10.10.0.1 tunnel address, and uses a 120s timeout.
wechat-egress-fetch.mjs rewrites WeChat API URLs to the egress base when
MEMIND_WECHAT_EGRESS_BASE_URL is set.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-23 10:07:32 +08:00
parent 028bb18dc0
commit e5cbe80644
3 changed files with 65 additions and 18 deletions
+15 -17
View File
@@ -1,23 +1,18 @@
#!/usr/bin/env python3
"""105 固定 IP 出站:仅转发 api.weixin.qq.com/cgi-bin/* 到微信官方 API。"""
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
import urllib.error
import urllib.parse
import urllib.request
ALLOWED_PATHS = {
"/cgi-bin/stable_token",
"/cgi-bin/message/custom/send",
"/cgi-bin/menu/create",
"/cgi-bin/menu/get",
"/cgi-bin/ticket/getticket",
}
ALLOWED_PREFIX = "/cgi-bin/"
TIMEOUT_SEC = 120
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/healthz":
if self.path == "/healthz" or self.path.startswith("/healthz?"):
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
@@ -30,29 +25,32 @@ class Handler(BaseHTTPRequestHandler):
def _proxy(self, method):
parsed = urllib.parse.urlsplit(self.path)
if parsed.path not in ALLOWED_PATHS:
if not parsed.path.startswith(ALLOWED_PREFIX):
self.send_error(403)
return
length = int(self.headers.get("Content-Length", "0")) if method == "POST" else 0
body = self.rfile.read(length) if length else None
url = "https://api.weixin.qq.com" + self.path
headers = {
"Content-Type": self.headers.get("Content-Type", "application/json"),
}
headers = {}
content_type = self.headers.get("Content-Type")
if content_type:
headers["Content-Type"] = content_type
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=20) as resp:
with urllib.request.urlopen(req, timeout=TIMEOUT_SEC) as resp:
payload = resp.read()
self.send_response(resp.status)
self.send_header("Content-Type", resp.headers.get("Content-Type", "application/json"))
out_type = resp.headers.get("Content-Type", "application/json")
self.send_header("Content-Type", out_type)
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
except urllib.error.HTTPError as err:
payload = err.read()
self.send_response(err.code)
self.send_header("Content-Type", err.headers.get("Content-Type", "application/json"))
out_type = err.headers.get("Content-Type", "application/json")
self.send_header("Content-Type", out_type)
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
@@ -62,6 +60,6 @@ class Handler(BaseHTTPRequestHandler):
if __name__ == "__main__":
host = os.environ.get("WECHAT_EGRESS_HOST", "127.0.0.1")
host = os.environ.get("WECHAT_EGRESS_HOST", "10.10.0.1")
port = int(os.environ.get("WECHAT_EGRESS_PORT", "19090"))
HTTPServer((host, port), Handler).serve_forever()