#!/usr/bin/env python3 """前端静态文件服务器 + API 反向代理(支持 SSE 流式响应)。""" import os import http.server import socketserver import http.client from urllib.parse import urlparse PORT = int(os.getenv('PORT', 8080)) DIRECTORY = '/app' # 后端 API 地址 BACKEND_URL = os.getenv('BACKEND_URL', 'http://baodanagent-api:5001') # 需要代理到后端的路径前缀 PROXY_PREFIXES = ['/insurance', '/api', '/v1'] # 解析后端地址 _parsed = urlparse(BACKEND_URL) BACKEND_HOST = _parsed.hostname BACKEND_PORT = _parsed.port or (443 if _parsed.scheme == 'https' else 80) BACKEND_TLS = _parsed.scheme == 'https' class Handler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=DIRECTORY, **kwargs) def do_GET(self): if self._proxy_request(): return self._serve_static() def do_POST(self): if self._proxy_request(): return self.send_error(404) def do_PUT(self): if self._proxy_request(): return self.send_error(404) def do_DELETE(self): if self._proxy_request(): return self.send_error(404) def do_OPTIONS(self): if self._proxy_request(): return self.send_error(404) def _proxy_request(self): """将 API 请求代理到后端。返回 True 表示已处理。""" for prefix in PROXY_PREFIXES: if self.path.startswith(prefix): self._forward_to_backend() return True return False def _forward_to_backend(self): """转发请求到后端服务(支持 SSE 流式响应)。""" # 读取请求体 content_length = int(self.headers.get('Content-Length', 0)) body = self.rfile.read(content_length) if content_length > 0 else None # 构建请求头(不转发 Content-Length,由 http.client 自动设置) headers = {} for key in ['Content-Type', 'Authorization', 'Accept', 'Cookie']: val = self.headers.get(key) if val: headers[key] = val headers['Host'] = BACKEND_HOST headers['Connection'] = 'keep-alive' conn = None try: # 建立到后端的连接 if BACKEND_TLS: conn = http.client.HTTPSConnection(BACKEND_HOST, BACKEND_PORT, timeout=120) else: conn = http.client.HTTPConnection(BACKEND_HOST, BACKEND_PORT, timeout=120) conn.request(self.command, self.path, body=body, headers=headers) resp = conn.getresponse() # 检测是否为 SSE 流式响应 content_type = resp.getheader('Content-Type', '') is_sse = 'text/event-stream' in content_type # 转发响应头 self.send_response(resp.status) for key, val in resp.getheaders(): key_lower = key.lower() if key_lower not in ('transfer-encoding', 'connection'): self.send_header(key, val) self._send_cors_headers() self.end_headers() # 流式转发响应体 if is_sse: # SSE: 逐行读取,实时转发 while True: line = resp.readline() if not line: break self.wfile.write(line) self.wfile.flush() else: # 普通响应:分块读取 while True: chunk = resp.read(8192) if not chunk: break self.wfile.write(chunk) self.wfile.flush() except Exception as e: try: self.send_response(502) self.send_header('Content-Type', 'application/json') self._send_cors_headers() self.end_headers() import json self.wfile.write(json.dumps({ "code": 502, "message": f"后端服务不可用: {str(e)}", "data": None }).encode()) except Exception: pass finally: if conn: try: conn.close() except Exception: pass def _serve_static(self): """服务静态文件。""" if self.path == '/': self.path = '/index.html' return super().do_GET() file_path = os.path.join(DIRECTORY, self.path.lstrip('/')) if os.path.exists(file_path) and not os.path.isdir(file_path): return super().do_GET() # SPA 路由回退 self.path = '/index.html' return super().do_GET() def _send_cors_headers(self): """发送 CORS 头。""" self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization') def log_message(self, format, *args): print(f"[前端服务] {format % args}") if __name__ == '__main__': print(f"前端服务启动,端口: {PORT}") print(f"服务目录: {DIRECTORY}") print(f"后端地址: {BACKEND_URL}") print(f"代理路径: {PROXY_PREFIXES}") socketserver.TCPServer.allow_reuse_address = True with socketserver.ThreadingTCPServer(("", PORT), Handler) as httpd: httpd.serve_forever()