mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-29 22:02:05 +00:00
Add support for using alternative API endpoints (like Zhipu AI's GLM models) without affecting the user's global Claude Code settings. Configuration is done via AutoCoder's .env file. Changes: - Add API_ENV_VARS constant and pass through ClaudeAgentOptions.env parameter in client.py and all server service files (spec, expand, assistant sessions) - Add glm_mode to settings API response to indicate when GLM is configured - Add purple "GLM" badge in UI header when GLM mode is active - Update setup status to accept GLM credentials as valid authentication - Update .env.example with GLM configuration documentation - Update README.md with AutoCoder-scoped GLM setup instructions Supported environment variables: - ANTHROPIC_BASE_URL: Custom API endpoint (e.g., https://api.z.ai/api/anthropic) - ANTHROPIC_AUTH_TOKEN: API authentication token - API_TIMEOUT_MS: Request timeout in milliseconds - ANTHROPIC_DEFAULT_SONNET_MODEL: Model override for Sonnet - ANTHROPIC_DEFAULT_OPUS_MODEL: Model override for Opus - ANTHROPIC_DEFAULT_HAIKU_MODEL: Model override for Haiku This approach routes API requests through the alternative endpoint while keeping all Claude Code features (MCP servers, hooks, permissions) intact. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
214 lines
6.7 KiB
Python
214 lines
6.7 KiB
Python
"""
|
|
FastAPI Main Application
|
|
========================
|
|
|
|
Main entry point for the Autonomous Coding UI server.
|
|
Provides REST API, WebSocket, and static file serving.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables from .env file if present
|
|
load_dotenv()
|
|
|
|
from fastapi import FastAPI, HTTPException, Request, WebSocket
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from .routers import (
|
|
agent_router,
|
|
assistant_chat_router,
|
|
devserver_router,
|
|
expand_project_router,
|
|
features_router,
|
|
filesystem_router,
|
|
projects_router,
|
|
settings_router,
|
|
spec_creation_router,
|
|
terminal_router,
|
|
)
|
|
from .schemas import SetupStatus
|
|
from .services.assistant_chat_session import cleanup_all_sessions as cleanup_assistant_sessions
|
|
from .services.dev_server_manager import (
|
|
cleanup_all_devservers,
|
|
cleanup_orphaned_devserver_locks,
|
|
)
|
|
from .services.expand_chat_session import cleanup_all_expand_sessions
|
|
from .services.process_manager import cleanup_all_managers, cleanup_orphaned_locks
|
|
from .services.terminal_manager import cleanup_all_terminals
|
|
from .websocket import project_websocket
|
|
|
|
# Paths
|
|
ROOT_DIR = Path(__file__).parent.parent
|
|
UI_DIST_DIR = ROOT_DIR / "ui" / "dist"
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Lifespan context manager for startup and shutdown."""
|
|
# Startup - clean up orphaned lock files from previous runs
|
|
cleanup_orphaned_locks()
|
|
cleanup_orphaned_devserver_locks()
|
|
yield
|
|
# Shutdown - cleanup all running agents, sessions, terminals, and dev servers
|
|
await cleanup_all_managers()
|
|
await cleanup_assistant_sessions()
|
|
await cleanup_all_expand_sessions()
|
|
await cleanup_all_terminals()
|
|
await cleanup_all_devservers()
|
|
|
|
|
|
# Create FastAPI app
|
|
app = FastAPI(
|
|
title="Autonomous Coding UI",
|
|
description="Web UI for the Autonomous Coding Agent",
|
|
version="1.0.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
# CORS - allow only localhost origins for security
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[
|
|
"http://localhost:5173", # Vite dev server
|
|
"http://127.0.0.1:5173",
|
|
"http://localhost:8888", # Production
|
|
"http://127.0.0.1:8888",
|
|
],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Security Middleware
|
|
# ============================================================================
|
|
|
|
@app.middleware("http")
|
|
async def require_localhost(request: Request, call_next):
|
|
"""Only allow requests from localhost."""
|
|
client_host = request.client.host if request.client else None
|
|
|
|
# Allow localhost connections
|
|
if client_host not in ("127.0.0.1", "::1", "localhost", None):
|
|
raise HTTPException(status_code=403, detail="Localhost access only")
|
|
|
|
return await call_next(request)
|
|
|
|
|
|
# ============================================================================
|
|
# Include Routers
|
|
# ============================================================================
|
|
|
|
app.include_router(projects_router)
|
|
app.include_router(features_router)
|
|
app.include_router(agent_router)
|
|
app.include_router(devserver_router)
|
|
app.include_router(spec_creation_router)
|
|
app.include_router(expand_project_router)
|
|
app.include_router(filesystem_router)
|
|
app.include_router(assistant_chat_router)
|
|
app.include_router(settings_router)
|
|
app.include_router(terminal_router)
|
|
|
|
|
|
# ============================================================================
|
|
# WebSocket Endpoint
|
|
# ============================================================================
|
|
|
|
@app.websocket("/ws/projects/{project_name}")
|
|
async def websocket_endpoint(websocket: WebSocket, project_name: str):
|
|
"""WebSocket endpoint for real-time project updates."""
|
|
await project_websocket(websocket, project_name)
|
|
|
|
|
|
# ============================================================================
|
|
# Setup & Health Endpoints
|
|
# ============================================================================
|
|
|
|
@app.get("/api/health")
|
|
async def health_check():
|
|
"""Health check endpoint."""
|
|
return {"status": "healthy"}
|
|
|
|
|
|
@app.get("/api/setup/status", response_model=SetupStatus)
|
|
async def setup_status():
|
|
"""Check system setup status."""
|
|
# Check for Claude CLI
|
|
claude_cli = shutil.which("claude") is not None
|
|
|
|
# Check for CLI configuration directory
|
|
# Note: CLI no longer stores credentials in ~/.claude/.credentials.json
|
|
# The existence of ~/.claude indicates the CLI has been configured
|
|
claude_dir = Path.home() / ".claude"
|
|
has_claude_config = claude_dir.exists() and claude_dir.is_dir()
|
|
|
|
# If GLM mode is configured via .env, we have alternative credentials
|
|
glm_configured = bool(os.getenv("ANTHROPIC_BASE_URL") and os.getenv("ANTHROPIC_AUTH_TOKEN"))
|
|
credentials = has_claude_config or glm_configured
|
|
|
|
# Check for Node.js and npm
|
|
node = shutil.which("node") is not None
|
|
npm = shutil.which("npm") is not None
|
|
|
|
return SetupStatus(
|
|
claude_cli=claude_cli,
|
|
credentials=credentials,
|
|
node=node,
|
|
npm=npm,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Static File Serving (Production)
|
|
# ============================================================================
|
|
|
|
# Serve React build files if they exist
|
|
if UI_DIST_DIR.exists():
|
|
# Mount static assets
|
|
app.mount("/assets", StaticFiles(directory=UI_DIST_DIR / "assets"), name="assets")
|
|
|
|
@app.get("/")
|
|
async def serve_index():
|
|
"""Serve the React app index.html."""
|
|
return FileResponse(UI_DIST_DIR / "index.html")
|
|
|
|
@app.get("/{path:path}")
|
|
async def serve_spa(path: str):
|
|
"""
|
|
Serve static files or fall back to index.html for SPA routing.
|
|
"""
|
|
# Check if the path is an API route (shouldn't hit this due to router ordering)
|
|
if path.startswith("api/") or path.startswith("ws/"):
|
|
raise HTTPException(status_code=404)
|
|
|
|
# Try to serve the file directly
|
|
file_path = UI_DIST_DIR / path
|
|
if file_path.exists() and file_path.is_file():
|
|
return FileResponse(file_path)
|
|
|
|
# Fall back to index.html for SPA routing
|
|
return FileResponse(UI_DIST_DIR / "index.html")
|
|
|
|
|
|
# ============================================================================
|
|
# Main Entry Point
|
|
# ============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(
|
|
"server.main:app",
|
|
host="127.0.0.1", # Localhost only for security
|
|
port=8888,
|
|
reload=True,
|
|
)
|