baodan/serve.py

171 lines
5.6 KiB
Python
Raw Normal View History

#!/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))
2026-07-23 15:04:16 +08:00
DIRECTORY = '/app/dist'
# 后端 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()