baodan/serve.py

171 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()