2026-07-02 23:13:23 +08:00
|
|
|
|
#!/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'
|
2026-07-02 23:13:23 +08:00
|
|
|
|
# 后端 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)
|
|
|
|
|
|
|
2026-07-30 13:54:44 +08:00
|
|
|
|
def do_PATCH(self):
|
|
|
|
|
|
if self._proxy_request():
|
|
|
|
|
|
return
|
|
|
|
|
|
self.send_error(404)
|
|
|
|
|
|
|
2026-07-02 23:13:23 +08:00
|
|
|
|
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', '*')
|
2026-07-30 13:54:44 +08:00
|
|
|
|
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
2026-07-02 23:13:23 +08:00
|
|
|
|
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()
|