#!/usr/bin/env python3 """Serve a SPA's dist/ directory + proxy API paths to a backend. Usage: python3 proxy-server.py [--port PORT] [--dist DIST_DIR] [--backend BACKEND_URL] Proxies: /pb/* → BACKEND/api/* (PocketBase SDK path) /api/* → BACKEND/api/* (direct API) /deepseek/* → https://api.deepseek.com/* (with API key from /tmp/deepseek_key.txt) /* → SPA static files (fallback to index.html for client-side routes) Environment: DEEPSEEK_KEY — API key for DeepSeek (overrides /tmp/deepseek_key.txt) """ from http.server import HTTPServer, SimpleHTTPRequestHandler import urllib.request, os, sys, argparse def parse_args(): p = argparse.ArgumentParser(description='SPA dev server with API proxy') p.add_argument('--port', type=int, default=4173) p.add_argument('--dist', default=os.path.join(os.path.dirname(__file__), 'dist')) p.add_argument('--backend', default='http://127.0.0.1:8091') return p.parse_args() ARGS = parse_args() DIST = ARGS.dist PB = ARGS.backend DEEPSEEK_KEY = os.environ.get('DEEPSEEK_KEY') if not DEEPSEEK_KEY: try: with open('/tmp/deepseek_key.txt') as f: DEEPSEEK_KEY = f.read().strip() except FileNotFoundError: pass class Handler(SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=DIST, **kwargs) def end_headers(self): self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type') super().end_headers() def do_OPTIONS(self): self.send_response(204) self.end_headers() def do_GET(self): if self._is_api_path(): self._proxy() else: full = os.path.join(DIST, self.path.lstrip('/')) if not os.path.exists(full) or (os.path.isdir(full) and self.path != '/'): self.path = '/index.html' super().do_GET() def do_POST(self): if self._is_api_path(): self._proxy() else: self.send_error(405) def do_PUT(self): self._proxy() def do_PATCH(self): self._proxy() def do_DELETE(self): self._proxy() def _is_api_path(self): return self.path.startswith(('/pb', '/api', '/deepseek', '/llm', '/vision')) def _proxy(self): if self.path.startswith('/deepseek'): url = 'https://api.deepseek.com' + self.path[len('/deepseek'):] elif self.path.startswith('/llm') or self.path.startswith('/vision'): url = 'http://127.0.0.1:11434' + self.path.split('/', 2)[-1] if '/' in self.path[1:] else self.path url = f'http://127.0.0.1:11434/{url}' if not url.startswith('http') else url elif self.path.startswith('/pb'): rest = self.path[3:] # /pb/api/collections/... → /api/collections/... url = PB + rest if not rest.startswith('/api'): url = PB + '/api' + rest else: url = PB + self.path body = None if self.headers.get('Content-Length'): body = self.rfile.read(int(self.headers['Content-Length'])) req = urllib.request.Request(url, data=body, method=self.command) for k, v in self.headers.items(): if k.lower() not in ('host', 'connection', 'origin', 'referer'): req.add_header(k, v) if self.path.startswith('/deepseek') and DEEPSEEK_KEY: req.add_header('Authorization', f'Bearer {DEEPSEEK_KEY}') try: resp = urllib.request.urlopen(req) self.send_response(resp.status) for k, v in resp.headers.items(): if k.lower() not in ('transfer-encoding', 'connection'): self.send_header(k, v) self.end_headers() self.wfile.write(resp.read()) except urllib.error.HTTPError as e: self.send_response(e.code) self.end_headers() self.wfile.write(e.read()) if __name__ == '__main__': server = HTTPServer(('0.0.0.0', ARGS.port), Handler) print(f'Serving {DIST} on http://0.0.0.0:{ARGS.port} (API → {PB})') server.serve_forever()