171 lines
5.6 KiB
Python
171 lines
5.6 KiB
Python
#!/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()
|