From b3ffdc72967001c04217b9f9e4c172e71be19668 Mon Sep 17 00:00:00 2001 From: tangwang Date: Tue, 14 Apr 2026 20:49:25 +0800 Subject: [PATCH] Sync legacy frontend entrypoint from 0a440fb --- scripts/frontend_server.py | 278 +++++++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1 file changed, 7 insertions(+), 271 deletions(-) mode change 100755 => 100644 scripts/frontend_server.py diff --git a/scripts/frontend_server.py b/scripts/frontend_server.py old mode 100755 new mode 100644 index 77dac02..95b4c04 --- a/scripts/frontend_server.py +++ b/scripts/frontend_server.py @@ -1,276 +1,12 @@ #!/usr/bin/env python3 -""" -Simple HTTP server for saas-search frontend. -""" +"""Backward-compatible frontend server entrypoint.""" -import http.server -import socketserver -import os -import sys -import logging -import time -import urllib.request -import urllib.error -from collections import defaultdict, deque -from pathlib import Path -from dotenv import load_dotenv - -# Load .env file -project_root = Path(__file__).parent.parent -load_dotenv(project_root / '.env') - -# Get API_BASE_URL from environment(默认不注入,避免被旧 .env 覆盖同源策略) -# 仅当显式设置 FRONTEND_INJECT_API_BASE_URL=1 时才注入 window.API_BASE_URL。 -API_BASE_URL = os.getenv('API_BASE_URL') or None -INJECT_API_BASE_URL = os.getenv('FRONTEND_INJECT_API_BASE_URL', '0') == '1' -# Backend proxy target for same-origin API forwarding -BACKEND_PROXY_URL = os.getenv('BACKEND_PROXY_URL', 'http://127.0.0.1:6002').rstrip('/') - -# Change to frontend directory -frontend_dir = os.path.join(os.path.dirname(__file__), '../frontend') -os.chdir(frontend_dir) - -# FRONTEND_PORT is the canonical config; keep PORT as a secondary fallback. -PORT = int(os.getenv('FRONTEND_PORT', os.getenv('PORT', 6003))) - -# Configure logging to suppress scanner noise -logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s') - -class RateLimitingMixin: - """Mixin for rate limiting requests by IP address.""" - request_counts = defaultdict(deque) - rate_limit = 100 # requests per minute - window = 60 # seconds - - @classmethod - def is_rate_limited(cls, ip): - now = time.time() - - # Clean old requests - while cls.request_counts[ip] and cls.request_counts[ip][0] < now - cls.window: - cls.request_counts[ip].popleft() - - # Check rate limit - if len(cls.request_counts[ip]) > cls.rate_limit: - return True - - cls.request_counts[ip].append(now) - return False - -class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler, RateLimitingMixin): - """Custom request handler with CORS support and robust error handling.""" - - def _is_proxy_path(self, path: str) -> bool: - """Return True for API paths that should be forwarded to backend service.""" - return path.startswith('/search/') or path.startswith('/admin/') or path.startswith('/indexer/') - - def _proxy_to_backend(self): - """Proxy current request to backend service on the GPU server.""" - target_url = f"{BACKEND_PROXY_URL}{self.path}" - method = self.command.upper() - - try: - content_length = int(self.headers.get('Content-Length', '0')) - except ValueError: - content_length = 0 - body = self.rfile.read(content_length) if content_length > 0 else None +from __future__ import annotations - forward_headers = {} - for key, value in self.headers.items(): - lk = key.lower() - if lk in ('host', 'content-length', 'connection'): - continue - forward_headers[key] = value - - req = urllib.request.Request( - target_url, - data=body, - headers=forward_headers, - method=method, - ) - - try: - with urllib.request.urlopen(req, timeout=30) as resp: - resp_body = resp.read() - self.send_response(resp.getcode()) - for header, value in resp.getheaders(): - lh = header.lower() - if lh in ('transfer-encoding', 'connection', 'content-length'): - continue - self.send_header(header, value) - self.end_headers() - self.wfile.write(resp_body) - except urllib.error.HTTPError as e: - err_body = e.read() if hasattr(e, 'read') else b'' - self.send_response(e.code) - if e.headers: - for header, value in e.headers.items(): - lh = header.lower() - if lh in ('transfer-encoding', 'connection', 'content-length'): - continue - self.send_header(header, value) - self.end_headers() - if err_body: - self.wfile.write(err_body) - except Exception as e: - logging.error(f"Backend proxy error for {method} {self.path}: {e}") - self.send_response(502) - self.send_header('Content-Type', 'application/json; charset=utf-8') - self.end_headers() - self.wfile.write(b'{"error":"Bad Gateway: backend proxy failed"}') - - def do_GET(self): - """Handle GET requests with API config injection.""" - path = self.path.split('?')[0] - - # Proxy API paths to backend first - if self._is_proxy_path(path): - self._proxy_to_backend() - return - - # Route / to index.html - if path == '/' or path == '': - self.path = '/index.html' + (self.path.split('?', 1)[1] if '?' in self.path else '') - - # Inject API config for HTML files - if self.path.endswith('.html'): - self._serve_html_with_config() - else: - super().do_GET() - - def _serve_html_with_config(self): - """Serve HTML with optional API_BASE_URL injected.""" - try: - file_path = self.path.lstrip('/') - if not os.path.exists(file_path): - self.send_error(404) - return - - with open(file_path, 'r', encoding='utf-8') as f: - html = f.read() - - # 默认不注入 API_BASE_URL,避免历史 .env(如 http://xx:6002)覆盖同源调用。 - # 仅当 FRONTEND_INJECT_API_BASE_URL=1 且 API_BASE_URL 有值时才注入。 - if INJECT_API_BASE_URL and API_BASE_URL: - config_script = f'\n ' - html = html.replace('