#!/usr/bin/env python3 """ AI Dev System - Web Dashboard Web 控制面板 """ from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import Optional, List import sqlite3 from pathlib import Path import os import subprocess from datetime import datetime # 導入資料庫管理器 import sys sys.path.append(str(Path(__file__).parent)) from db_manager import ( DB_PATH, get_execution_history, get_system_stats, get_project_config, update_project_config ) from load_project_config import ProjectConfigLoader app = FastAPI(title="AI Dev System Dashboard", version="1.0.0") # CORS 設定(允許從任何來源訪問) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ============================================ # Pydantic Models # ============================================ class ProjectConfig(BaseModel): project_name: str project_dir: str ai_provider: str = "claude" is_enabled: bool = True max_iterations: int = 50 class EnvironmentVar(BaseModel): var_name: str var_value: str var_description: Optional[str] = None is_sensitive: bool = False class PromptUpdate(BaseModel): project_name: str prompt_content: str # ============================================ # API Endpoints # ============================================ @app.get("/") async def root(): """返回 Web Dashboard HTML""" html_file = Path(__file__).parent / "dashboard.html" if html_file.exists(): return HTMLResponse(content=html_file.read_text()) return {"message": "AI Dev System API", "docs": "/docs"} @app.get("/api/status") async def get_status(): """獲取系統狀態""" try: stats = get_system_stats() # 讀取當前環境變數 env_file = Path.home() / ".ai-dev-env" current_env = {} if env_file.exists(): for line in env_file.read_text().split('\n'): if line.startswith('export '): line = line.replace('export ', '') if '=' in line: key, value = line.split('=', 1) current_env[key] = value.strip('"') return { "system_status": stats['status'], "project_stats": stats['projects'], "current_env": current_env, "timestamp": datetime.now().isoformat() } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/executions") async def get_executions(project: Optional[str] = None, limit: int = 50): """獲取執行歷史""" try: history = get_execution_history(project, limit) return {"executions": history, "count": len(history)} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/executions/{execution_id}") async def get_execution_detail(execution_id: int): """獲取單次執行詳情""" try: conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(""" SELECT * FROM execution_history WHERE id = ? """, (execution_id,)) result = cursor.fetchone() conn.close() if not result: raise HTTPException(status_code=404, detail="Execution not found") return dict(result) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/projects") async def get_projects(): """獲取所有專案配置(從 projects.yml 讀取)""" try: config_file = Path(__file__).parent / "projects.yml" loader = ProjectConfigLoader(str(config_file)) projects = [] for project_id in loader.list_projects(): project_config = loader.get_project(project_id) if project_config: # 讀取專案狀態 project_dir = Path(os.path.expandvars(project_config.get('project_dir', ''))) ai_dev_dir = project_dir / ".ai-dev" # 讀取迭代次數 iteration_file = ai_dev_dir / "iteration.txt" current_iteration = 0 if iteration_file.exists(): try: current_iteration = int(iteration_file.read_text().strip()) except: pass # 檢查是否已完成 is_completed = (ai_dev_dir / "completed").exists() projects.append({ "id": project_id, "project_name": project_config.get('name', project_id), "project_dir": project_config.get('project_dir', ''), "ai_provider": project_config.get('ai_provider', 'claude'), "is_enabled": project_config.get('enable', True), "max_iterations": project_config.get('max_iterations', 30), "current_iteration": current_iteration, "is_completed": is_completed, "description": project_config.get('description', '') }) return {"projects": projects} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/projects/{project_name}") async def get_project(project_name: str): """獲取專案詳情(從 projects.yml 讀取)""" try: config_file = Path(__file__).parent / "projects.yml" loader = ProjectConfigLoader(str(config_file)) project_config = loader.get_project(project_name) if not project_config: raise HTTPException(status_code=404, detail="Project not found") # 展開環境變數 project_dir = Path(os.path.expandvars(project_config.get('project_dir', ''))) ai_dev_dir = project_dir / ".ai-dev" # 讀取 prompt prompt_file = ai_dev_dir / "prompt.txt" prompt_content = "" if prompt_file.exists(): prompt_content = prompt_file.read_text() # 讀取迭代次數 iteration_file = ai_dev_dir / "iteration.txt" current_iteration = 0 if iteration_file.exists(): try: current_iteration = int(iteration_file.read_text().strip()) except: pass # 檢查是否已完成 is_completed = (ai_dev_dir / "completed").exists() return { "config": { "id": project_name, "project_name": project_config.get('name', project_name), "project_dir": project_config.get('project_dir', ''), "ai_provider": project_config.get('ai_provider', 'claude'), "is_enabled": project_config.get('enable', True), "max_iterations": project_config.get('max_iterations', 30), "current_iteration": current_iteration, "is_completed": is_completed, "description": project_config.get('description', '') }, "prompt": prompt_content } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/projects/{project_name}") async def update_project(project_name: str, config: ProjectConfig): """更新專案配置""" try: update_project_config( project_name, project_dir=config.project_dir, ai_provider=config.ai_provider, is_enabled=config.is_enabled, max_iterations=config.max_iterations ) return {"message": "Project updated successfully"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/projects/{project_name}/status") async def get_project_status(project_name: str): """獲取專案狀態詳情""" try: config_file = Path(__file__).parent / "projects.yml" loader = ProjectConfigLoader(str(config_file)) project_config = loader.get_project(project_name) if not project_config: raise HTTPException(status_code=404, detail="Project not found") # 展開環境變數 project_dir = Path(os.path.expandvars(project_config.get('project_dir', ''))) ai_dev_dir = project_dir / ".ai-dev" # 讀取迭代次數 iteration_file = ai_dev_dir / "iteration.txt" current_iteration = 0 if iteration_file.exists(): try: current_iteration = int(iteration_file.read_text().strip()) except: pass # 檢查是否已完成 is_completed = (ai_dev_dir / "completed").exists() # 讀取最近的日誌 logs_dir = ai_dev_dir / "logs" recent_logs = [] if logs_dir.exists(): log_files = sorted(logs_dir.glob("session_*.log"), reverse=True)[:5] for log_file in log_files: recent_logs.append({ "filename": log_file.name, "modified": datetime.fromtimestamp(log_file.stat().st_mtime).isoformat() }) # 讀取最近的輸出 outputs_dir = ai_dev_dir / "outputs" recent_outputs = [] if outputs_dir.exists(): output_files = sorted(outputs_dir.glob("output_*.txt"), reverse=True)[:5] for output_file in output_files: recent_outputs.append({ "filename": output_file.name, "modified": datetime.fromtimestamp(output_file.stat().st_mtime).isoformat() }) # 檢查專案目錄是否存在 project_exists = project_dir.exists() return { "project_id": project_name, "project_name": project_config.get('name', project_name), "project_dir": str(project_dir), "project_exists": project_exists, "ai_provider": project_config.get('ai_provider', 'claude'), "is_enabled": project_config.get('enable', True), "max_iterations": project_config.get('max_iterations', 30), "current_iteration": current_iteration, "is_completed": is_completed, "recent_logs": recent_logs, "recent_outputs": recent_outputs, "ai_dev_dir_exists": ai_dev_dir.exists() } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/projects/{project_name}/prompt") async def update_prompt(project_name: str, data: PromptUpdate): """更新專案 Prompt""" try: config_file = Path(__file__).parent / "projects.yml" loader = ProjectConfigLoader(str(config_file)) project_config = loader.get_project(project_name) if not project_config: raise HTTPException(status_code=404, detail="Project not found") project_dir = Path(os.path.expandvars(project_config.get('project_dir', ''))) ai_dev_dir = project_dir / ".ai-dev" ai_dev_dir.mkdir(parents=True, exist_ok=True) prompt_file = ai_dev_dir / "prompt.txt" prompt_file.write_text(data.prompt_content) return {"message": "Prompt updated successfully"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/env") async def get_environment_variables(): """獲取所有環境變數""" try: conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM environment_variables ORDER BY var_name") variables = [dict(row) for row in cursor.fetchall()] conn.close() # 也讀取當前的 .ai-dev-env env_file = Path.home() / ".ai-dev-env" current_env = {} if env_file.exists(): for line in env_file.read_text().split('\n'): if line.startswith('export '): line = line.replace('export ', '') if '=' in line: key, value = line.split('=', 1) current_env[key] = value.strip('"') return { "variables": variables, "current_env": current_env } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/env") async def update_environment_variable(var: EnvironmentVar): """更新環境變數""" try: conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" INSERT OR REPLACE INTO environment_variables (var_name, var_value, var_description, is_sensitive, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) """, (var.var_name, var.var_value, var.var_description, var.is_sensitive)) conn.commit() conn.close() # 更新 .ai-dev-env 檔案 env_file = Path.home() / ".ai-dev-env" lines = [] if env_file.exists(): lines = env_file.read_text().split('\n') # 更新或新增變數 found = False for i, line in enumerate(lines): if line.startswith(f'export {var.var_name}='): lines[i] = f'export {var.var_name}="{var.var_value}"' found = True break if not found: lines.append(f'export {var.var_name}="{var.var_value}"') env_file.write_text('\n'.join(lines)) return {"message": "Environment variable updated successfully"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/control/start/{project_name}") async def start_project(project_name: str): """啟動專案""" try: script_dir = Path(__file__).parent control_script = script_dir / "ai-dev-control.sh" result = subprocess.run( [str(control_script), "start", project_name], capture_output=True, text=True ) if result.returncode != 0: raise HTTPException(status_code=500, detail=result.stderr) return {"message": f"Project {project_name} started", "output": result.stdout} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/control/stop") async def stop_project(): """停止當前專案""" try: script_dir = Path(__file__).parent control_script = script_dir / "ai-dev-control.sh" result = subprocess.run( [str(control_script), "stop"], capture_output=True, text=True ) return {"message": "Project stopped", "output": result.stdout} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/control/run") async def run_now(): """立即執行一次""" try: script_dir = Path(__file__).parent control_script = script_dir / "ai-dev-control.sh" result = subprocess.run( [str(control_script), "run"], capture_output=True, text=True, timeout=300 # 5 分鐘超時 ) return {"message": "Execution completed", "output": result.stdout} except subprocess.TimeoutExpired: return {"message": "Execution started (running in background)"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/logs") async def get_logs(lines: int = 50, project: Optional[str] = None): """獲取最新的 logs""" try: # 如果指定專案,讀取專案的 master log if project: config = get_project_config(project) if config: log_file = Path(config['project_dir']) / ".ai-dev" / "logs" / "master.log" else: log_file = Path.home() / ".ai-dev-logs" / "master.log" else: # 預設讀取系統 log log_file = Path.home() / ".ai-dev-logs" / "master.log" if not log_file.exists(): return {"logs": ""} # 讀取最後 N 行 with open(log_file, 'r') as f: all_lines = f.readlines() recent_lines = all_lines[-lines:] return {"logs": ''.join(recent_lines)} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) if __name__ == "__main__": import uvicorn # 初始化資料庫 from db_manager import init_database init_database() # 從環境變數獲取端口,預設為 8000 port = int(os.environ.get('WEB_SERVER_PORT', 8000)) print("🚀 Starting AI Dev System Dashboard...") print(f"📊 Access at: http://localhost:{port}") print(f"📖 API docs: http://localhost:{port}/docs") uvicorn.run(app, host="0.0.0.0", port=port)