import os import pty import fcntl import subprocess import sqlite3 import asyncio import openai import re import httpx import struct import termios from datetime import datetime from pathlib import Path from telegram import Update from telegram.ext import ApplicationBuilder, ContextTypes, MessageHandler, filters from telegram.error import InvalidToken from google import genai from google.genai import types import json from fastapi import FastAPI, WebSocket, BackgroundTasks, Request, Form, WebSocketDisconnect from fastapi.responses import RedirectResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles from dotenv import load_dotenv, set_key # Basis-Verzeichnis (source/) BASE_DIR = Path(__file__).resolve().parent # Pfade zu den neuen Ordnern (eins hoch, dann in den Zielordner) ROOT_DIR = BASE_DIR.parent CONFIG_DIR = ROOT_DIR / "config" DATA_DIR = ROOT_DIR / "data" WORKSPACE_DIR = ROOT_DIR / "workspace" # Konfigurationsdateien ENV_FILE = CONFIG_DIR / ".env" load_dotenv(ENV_FILE) DB_PATH = DATA_DIR / "cluster.db" PROMPT_FILE = CONFIG_DIR / "system_prompt.txt" WEB_USER_NAME = os.getenv("WEB_USER_NAME", "Admin") # Workspace Dateien NOTES_FILE = WORKSPACE_DIR / "NOTIZEN.md" TODO_FILE = WORKSPACE_DIR / "TODO.md" # Sicherstellen, dass die Workspace-Dateien existieren WORKSPACE_DIR.mkdir(exist_ok=True) for f in [NOTES_FILE, TODO_FILE]: if not f.exists(): f.write_text(f"# {f.name}\nHier fängt dein Gedächtnis an, J.A.R.V.I.S.\n", encoding="utf-8") # Workspace-Ordner und Dateien anlegen WORKSPACE_DIR.mkdir(exist_ok=True) # Auch den Daten-Ordner für die Datenbank anlegen! DATA_DIR.mkdir(exist_ok=True) # FastAPI Pfade (relativ zu main.py in source/) app = FastAPI() #static_path = os.path.join(os.path.dirname(__file__), "static") templates = Jinja2Templates(directory=BASE_DIR / "templates") app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static") SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") # Ein Dictionary für verschiedene Chat-Historien chat_histories = { "private": [] # Hier landen Webchat UND private Telegram-Nachrichten } # KI KONFIGURATION AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower() OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "") NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "") OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1") GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-2.0-flash") OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o") OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3") NVIDIA_MODEL = os.getenv("NVIDIA_MODEL", "moonshotai/kimi-k2.5") # Telegram Bot Konfiguration TELEGRAM_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "") ALLOWED_ID = os.getenv("ALLOWED_TELEGRAM_USER_ID", "") telegram_app = None # --- DATENBANK INITIALISIERUNG (ERWEITERT) --- def init_db(): conn = sqlite3.connect(DB_PATH) # Spalten erweitert um sudo_password, os, arch, docker_installed conn.execute(''' CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, ip TEXT UNIQUE, user TEXT, sudo_password TEXT, os TEXT DEFAULT 'Unbekannt', arch TEXT DEFAULT 'Unbekannt', docker_installed INTEGER DEFAULT 0, status TEXT ) ''') conn.commit() conn.close() init_db() def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def get_system_prompt(current_user=WEB_USER_NAME, is_admin=False): # Entscheide, welche Datei geladen wird if is_admin: prompt_path = CONFIG_DIR / "system_prompt.txt" else: prompt_path = CONFIG_DIR / "group_prompt.txt" # Datei auslesen if prompt_path.exists(): prompt = prompt_path.read_text(encoding="utf-8") else: # Fallback, falls die Datei fehlt prompt = f"Hallo {current_user}, ich bin dein Assistent." # Namen ersetzen (funktioniert in beiden Prompts) prompt = prompt.replace("{user_name}", current_user) # Server-Infos NUR für Admins einfügen if is_admin: conn = get_db() nodes = conn.execute('SELECT * FROM nodes').fetchall() conn.close() node_info = "" for n in nodes: docker_str = "Ja" if n['docker_installed'] else "Nein" node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}, OS: {n['os']}, Arch: {n['arch']}, Docker: {docker_str}\n" prompt = prompt.replace("{node_info}", node_info) prompt = prompt.replace("{workspace_dir}", str(WORKSPACE_DIR)) prompt = prompt.replace("{notes_file}", str(NOTES_FILE)) prompt = prompt.replace("{todo_file}", str(TODO_FILE)) return prompt # --- KI FUNKTIONEN --- async def get_ai_response(user_msg, system_prompt, history_list): ai_msg = "" try: # Kombinierte Logik für OpenAI, Ollama und NVIDIA (alle nutzen das OpenAI SDK) if AI_PROVIDER in ["openai", "ollama", "nvidia"]: # WICHTIG: Wir nutzen die übergebene 'history_list', NICHT starr "private" # Da der Aufrufer (Telegram/Web) die aktuelle Frage schon hinzugefügt hat, ist sie hier schon drin. messages = [{"role": "system", "content": system_prompt}] + history_list if AI_PROVIDER == "ollama": url = OLLAMA_BASE_URL if not url.endswith('/v1') and not url.endswith('/v1/'): url = url.rstrip('/') + '/v1' key = "ollama" model_to_use = OLLAMA_MODEL elif AI_PROVIDER == "nvidia": url = "https://integrate.api.nvidia.com/v1" key = NVIDIA_API_KEY model_to_use = NVIDIA_MODEL else: # openai url = None key = OPENAI_API_KEY model_to_use = OPENAI_MODEL client = openai.AsyncOpenAI(base_url=url, api_key=key) response = await client.chat.completions.create( model=model_to_use, messages=messages ) ai_msg = response.choices[0].message.content elif AI_PROVIDER == "google": if not GOOGLE_API_KEY: return "Fehler: GOOGLE_API_KEY fehlt in der .env Datei!" client = genai.Client(api_key=GOOGLE_API_KEY) google_history = [] # Alle Nachrichten AUSSER der allerletzten (das ist die aktuelle Frage von gerade eben) for msg in history_list[:-1]: role = "user" if msg["role"] == "user" else "model" google_history.append( types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])]) ) chat = client.chats.create( model=GOOGLE_MODEL, config=types.GenerateContentConfig(system_instruction=system_prompt), history=google_history ) # Sende die aktuelle Nachricht an den Chat response = chat.send_message(user_msg) ai_msg = response.text except Exception as e: ai_msg = f"Fehler bei der KI-Anfrage: {e}" print(f"KI Fehler: {e}") # Wir geben nur den Text zurück. Das Speichern der KI-Antwort in die Historie # erledigt der Aufrufer (Web-Chat Endpoint oder Telegram Funktion)! return ai_msg async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_TYPE): # Den eigenen @Benutzernamen des Bots dynamisch abfragen bot_username = f"@{context.bot.username}" chat_type = update.effective_chat.type user_msg = update.message.text user_id = str(update.message.from_user.id) chat_id = str(update.effective_chat.id) # 1. Weiche für Gruppen-Logik & Historien-Zuordnung if chat_type in ['group', 'supergroup']: if bot_username not in user_msg: return # Bot wurde nicht erwähnt, Nachricht ignorieren # Den @Namen aus dem Text entfernen user_msg = user_msg.replace(bot_username, "").strip() # Jede Gruppe bekommt ihr komplett eigenes Gedächtnis history_key = chat_id else: # Sicherheitscheck im Einzelchat if user_id != ALLOWED_ID: await update.message.reply_text("Zugriff auf den privaten Chat verweigert. 🔒") return # Privat-Chats teilen sich das Gedächtnis mit dem Webchat history_key = "private" # Sicherstellen, dass die Liste für diesen Chat existiert if history_key not in chat_histories: chat_histories[history_key] = [] current_history = chat_histories[history_key] # Nutzer-Nachricht mit Zeitstempel in die RICHTIGE Historie aufnehmen now = datetime.now().strftime("%d.%m.%Y %H:%M") current_history.append({"role": "user", "content": user_msg, "timestamp": now}) # Vorname des Telegram-Nutzers auslesen sender_name = update.message.from_user.first_name # Tipp-Status anzeigen await update.message.reply_chat_action(action="typing") # 2. KI fragen (Wir übergeben die spezifische Historie!) # HINWEIS: Stelle sicher, dass deine get_ai_response Funktion die current_history auch annimmt. # Prüfen, ob der Sender der Admin ist is_admin_user = (user_id == ALLOWED_ID) # KI fragen (mit Admin-Flag für den Prompt!) ai_response = await get_ai_response( user_msg, get_system_prompt(sender_name, is_admin=is_admin_user), current_history ) commands = re.findall(r'(.*?)', ai_response, re.I | re.S) clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() # KI-Antwort ebenfalls in die Historie speichern now = datetime.now().strftime("%d.%m.%Y %H:%M") current_history.append({"role": "assistant", "content": clean_msg, "timestamp": now}) # KI Text-Antwort senden if clean_msg: await update.message.reply_text(clean_msg) # 3. Befehle ausführen (mit strengem Sicherheits-Check!) if commands: if user_id != ALLOWED_ID: await update.message.reply_text("⚠️ **Sicherheits-Sperre:** Die KI wollte einen Server-Befehl ausführen, aber du hast keine Administrator-Rechte dafür.") return for target, cmd in commands: await update.message.reply_text(f"⏳ Führe aus auf *{target}*:\n`{cmd}`", parse_mode='Markdown') # Node in DB suchen conn = get_db() n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone() conn.close() if n: try: proc = await asyncio.create_subprocess_shell( f"ssh -o StrictHostKeyChecking=no {n['user']}@{n['ip']} '{cmd}'", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT ) stdout, _ = await proc.communicate() output = stdout.decode('utf-8', errors='ignore').strip() result_text = output[:4000] if output else "✅ Befehl ohne Output ausgeführt." await update.message.reply_text(f"💻 **Output von {n['name']}:**\n```\n{result_text}\n```", parse_mode='Markdown') # System-Output auch in die RICHTIGE Historie packen, damit die KI weiß, was passiert ist now = datetime.now().strftime("%d.%m.%Y %H:%M") current_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {target} fertig:\n{result_text}", "timestamp": now}) except Exception as e: await update.message.reply_text(f"❌ Fehler bei der Ausführung: {e}") else: await update.message.reply_text(f"⚠️ Node '{target}' nicht in der Datenbank gefunden.") # Optionaler Bonus: Halte die Historie sauber (z.B. max. 15 Nachrichten), damit der RAM nicht überläuft if len(current_history) > 15: chat_histories[history_key] = current_history[-15:] # --- FASTAPI LIFESPAN EVENTS (Bot starten/stoppen) --- @app.on_event("startup") async def startup_event(): global telegram_app # Prüfe auch, ob der Token nicht aus Versehen noch der Platzhalter ist if TELEGRAM_TOKEN and ALLOWED_ID and "dein-telegram-bot-token" not in TELEGRAM_TOKEN: print("🤖 Starte Telegram Bot im Hintergrund...") try: telegram_app = ApplicationBuilder().token(TELEGRAM_TOKEN).build() # Leitet alle Text-Nachrichten an unsere Funktion weiter telegram_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_telegram_message)) # Bot asynchron in die FastAPI Event-Loop einhängen await telegram_app.initialize() await telegram_app.start() await telegram_app.updater.start_polling() print("✅ Telegram Bot lauscht!") except InvalidToken: print("❌ Telegram-Fehler: Der Token in der .env ist ungültig! Der Bot bleibt inaktiv, aber der Server läuft weiter.") except Exception as e: print(f"❌ Unerwarteter Fehler beim Telegram-Start: {e}") else: print("ℹ️ Telegram Bot inaktiv (Token oder ID fehlen/sind Platzhalter in der .env).") @app.on_event("shutdown") async def shutdown_event(): global telegram_app if telegram_app: print("🛑 Stoppe Telegram Bot...") await telegram_app.updater.stop() await telegram_app.stop() await telegram_app.shutdown() # --- WebSocket Manager --- class ConnectionManager: def __init__(self): self.active_connections = [] async def connect(self, ws: WebSocket): await ws.accept(); self.active_connections.append(ws) def disconnect(self, ws: WebSocket): self.active_connections.remove(ws) async def broadcast(self, msg: str): for c in self.active_connections: try: await c.send_text(msg) except: pass manager = ConnectionManager() async def get_remote_info(ip, user): """Versucht Linux/Mac-Infos zu lesen, falls fehlgeschlagen, dann Windows.""" # 1. Versuch: Linux/Mac linux_cmd = "uname -m && (sw_vers -productName 2>/dev/null || grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 || uname -s) && (command -v docker >/dev/null 2>&1 && echo 1 || echo 0)" ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{linux_cmd}\"" try: output = subprocess.check_output(ssh_cmd, shell=True, stderr=subprocess.DEVNULL).decode().strip().split('\n') if len(output) >= 2: return { "arch": output[0], "os": output[1].replace('"', ''), "docker": int(output[2]) if len(output) > 2 else 0 } except: pass # Linux-Versuch gescheitert, weiter zu Windows # 2. Versuch: Windows (CMD) # ver = OS Version, echo %PROCESSOR_ARCHITECTURE% = Arch, where docker = Docker Check win_cmd = 'ver && echo %PROCESSOR_ARCHITECTURE% && (where docker >nul 2>&1 && echo 1 || echo 0)' ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{win_cmd}\"" try: output = subprocess.check_output(ssh_cmd, shell=True).decode().strip().split('\n') # Windows Output sieht oft so aus: ["Microsoft Windows [Version 10.0...]", "AMD64", "1"] raw_os = output[0] if len(output) > 0 else "Windows" os_name = "Windows" if "Version 10" in raw_os: os_name = "Windows 10/11" elif "Version 11" in raw_os: os_name = "Windows 11" arch = output[1] if len(output) > 1 else "x86" if "AMD64" in arch: arch = "x86-64" docker_val = int(output[2]) if len(output) > 2 else 0 return {"arch": arch, "os": os_name, "docker": docker_val} except Exception as e: print(f"Windows-Check fehlgeschlagen für {ip}: {e}") return None # Nutze diese Funktion nun in bootstrap_node und refresh_status # --- ERWEITERTES NODE BOOTSTRAPPING (Inventur) --- async def bootstrap_node(ip, user, password): await manager.broadcast(f"🔑 Kopple {ip}...") with open(f"{SSH_KEY}.pub", "r") as f: pub_key = f.read().strip() # Wir nutzen ein absolut minimalistisches Kommando. # Es erstellt das Verzeichnis (falls nötig) und hängt den Key an. # Das funktioniert in der Windows CMD und der Linux Bash. cmd_universal = f'mkdir .ssh & echo {pub_key} >> .ssh/authorized_keys' # sshpass direkt mit dem simplen Befehl setup_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {user}@{ip} \"{cmd_universal}\"" try: # Wir führen es aus. Das "2x Passwort"-Problem kommt oft von TTY-Anfragen. # Wir unterdrücken das mit -o StrictHostKeyChecking=no proc = subprocess.run(setup_cmd, shell=True, capture_output=True, text=True, timeout=15) if proc.returncode == 0: await manager.broadcast(f"✅ Key an {ip} übertragen.") else: # Falls 'mkdir' einen Fehler wirft (weil Ordner existiert), ist das egal, # solange der Key danach drin ist. await manager.broadcast(f"ℹ️ Info: {ip} antwortet (Key-Check folgt).") except Exception as e: await manager.broadcast(f"❌ Fehler: {e}") # Inventur (get_remote_info) prüft jetzt, ob es wirklich klappt await manager.broadcast(f"🔍 Teste schlüssellosen Zugriff auf {ip}...") info = await get_remote_info(ip, user) if info: status = "Docker Aktiv" if info['docker'] else "Bereit (Kein Docker)" conn = get_db() conn.execute(''' UPDATE nodes SET os = ?, arch = ?, docker_installed = ?, status = ? WHERE ip = ? ''', (info['os'], info['arch'], info['docker'], status, ip)) conn.commit() conn.close() await manager.broadcast(f"✅ Node {ip} erkannt als {info['os']} ({info['arch']}).") else: await manager.broadcast(f"⚠️ Inventur auf {ip} fehlgeschlagen.") # --- ROUTES --- @app.get("/") async def index(request: Request): conn = get_db() nodes = conn.execute('SELECT * FROM nodes').fetchall() conn.close() return templates.TemplateResponse("index.html", {"request": request, "nodes": nodes}) @app.get("/api/node/{node_id}") async def get_node(node_id: int): conn = get_db() node = conn.execute('SELECT * FROM nodes WHERE id = ?', (node_id,)).fetchone() conn.close() return dict(node) if node else {} @app.put("/api/node/{node_id}") async def api_update_node(node_id: int, request: Request): data = await request.json() conn = get_db() try: conn.execute(''' UPDATE nodes SET name = ?, ip = ?, user = ?, sudo_password = ?, os = ?, arch = ?, status = ?, docker_installed = ? WHERE id = ? ''', ( data.get("name"), data.get("ip"), data.get("user"), data.get("sudo_password"), data.get("os"), data.get("arch"), data.get("status"), data.get("docker_installed"), node_id )) conn.commit() return {"status": "success"} except Exception as e: print(f"Update Fehler: {e}") return {"status": "error", "message": str(e)}, 500 finally: conn.close() @app.post("/add_node") async def add_node(background_tasks: BackgroundTasks, name: str = Form(...), ip: str = Form(...), user: str = Form(...), password: str = Form(...)): conn = get_db() try: # Speichere Initialdaten inkl. Sudo-Passwort conn.execute(''' INSERT INTO nodes (name, ip, user, sudo_password, status) VALUES (?, ?, ?, ?, ?) ''', (name, ip, user, password, "Kopplung...")) conn.commit() background_tasks.add_task(bootstrap_node, ip, user, password) except sqlite3.IntegrityError: pass finally: conn.close() return RedirectResponse(url="/", status_code=303) @app.post("/edit_node/{node_id}") async def edit_node(node_id: int, name: str = Form(...), ip: str = Form(...), user: str = Form(...)): conn = get_db() conn.execute('UPDATE nodes SET name=?, ip=?, user=? WHERE id=?', (name, ip, user, node_id)) conn.commit() conn.close() return RedirectResponse(url="/", status_code=303) @app.post("/remove_node/{node_id}") async def remove_node(node_id: int): conn = get_db() conn.execute('DELETE FROM nodes WHERE id = ?', (node_id,)) conn.commit() conn.close() return RedirectResponse(url="/", status_code=303) @app.get("/refresh_status/{node_id}") async def refresh_status(node_id: int): conn = get_db() node = conn.execute('SELECT * FROM nodes WHERE id = ?', (node_id,)).fetchone() if not node: return {"status": "Offline"} info = await get_remote_info(node['ip'], node['user']) if info: new_status = "Docker Aktiv" if info['docker'] else "Bereit (Kein Docker)" conn.execute('UPDATE nodes SET status=?, os=?, arch=?, docker_installed=? WHERE id=?', (new_status, info['os'], info['arch'], info['docker'], node_id)) conn.commit() result = {"status": new_status, "os": info['os'], "arch": info['arch'], "docker": info['docker']} else: new_status = "Offline" conn.execute('UPDATE nodes SET status=? WHERE id=?', (new_status, node_id)) conn.commit() result = {"status": new_status, "os": node['os'], "arch": node['arch'], "docker": node['docker_installed']} conn.close() return result # --- WebSockets Terminal / Chat / Logs (Integration wie gehabt) --- @app.websocket("/ws/install_logs") async def log_websocket(websocket: WebSocket): await manager.connect(websocket) try: while True: await websocket.receive_text() except WebSocketDisconnect: manager.disconnect(websocket) @app.websocket("/ws/terminal/{ip}") async def terminal_websocket(websocket: WebSocket, ip: str): await websocket.accept() conn = get_db() node = conn.execute('SELECT * FROM nodes WHERE ip = ?', (ip,)).fetchone() conn.close() if not node: await websocket.close() return master_fd, slave_fd = pty.openpty() # Wir starten SSH im interaktiven Modus proc = await asyncio.create_subprocess_exec( "ssh", "-i", SSH_KEY, # <--- Das hier ist entscheidend! "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes", # Verhindert, dass SSH hängen bleibt, falls der Key doch nicht geht "-t", f"{node['user']}@{ip}", stdin=slave_fd, stdout=slave_fd, stderr=slave_fd ) async def pty_to_ws(): fl = fcntl.fcntl(master_fd, fcntl.F_GETFL) fcntl.fcntl(master_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) try: while True: await asyncio.sleep(0.01) try: data = os.read(master_fd, 1024).decode(errors='ignore') if data: await websocket.send_text(data) except BlockingIOError: continue except: pass async def ws_to_pty(): try: while True: data = await websocket.receive_text() # Prüfen, ob es ein Resize-Kommando (JSON) ist if data.startswith('{"type":"resize"'): resize_data = json.loads(data) cols = resize_data['cols'] rows = resize_data['rows'] # Das hier setzt die Größe des PTY im Betriebssystem s = struct.pack("HHHH", rows, cols, 0, 0) fcntl.ioctl(master_fd, termios.TIOCSWINSZ, s) else: # Normale Terminal-Eingabe os.write(master_fd, data.encode()) except: pass try: await asyncio.gather(pty_to_ws(), ws_to_pty()) finally: if proc.returncode is None: proc.terminate() os.close(master_fd) os.close(slave_fd) @app.websocket("/ws/chat") async def chat_endpoint(websocket: WebSocket): await websocket.accept() # Sicherstellen, dass die private History existiert if "private" not in chat_histories: chat_histories["private"] = [] try: while True: user_msg = await websocket.receive_text() # 1. User-Nachricht in Historie speichern now = datetime.now().strftime("%d.%m.%Y %H:%M") chat_histories["private"].append({"role": "user", "content": user_msg, "timestamp": now}) # 2. KI fragen (mit der Historie!) ai_response = await get_ai_response(user_msg, get_system_prompt(is_admin=True), chat_histories["private"]) commands = re.findall(r'(.*?)', ai_response, re.I | re.S) clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() # 3. KI-Antwort in Historie speichern if clean_msg: now = datetime.now().strftime("%d.%m.%Y %H:%M") chat_histories["private"].append({"role": "assistant", "content": clean_msg, "timestamp": now}) await websocket.send_text(clean_msg) # Begrenzung der Historie if len(chat_histories["private"]) > 30: chat_histories["private"] = chat_histories["private"][-30:] if commands: tasks = [] for target, cmd in commands: conn = get_db() n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone() conn.close() if n: # Wir übergeben hier die History, damit run_remote_task weiß, wo er loggen soll tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip(), "private")) if tasks: await websocket.send_text("ℹ️ *Führe Befehle aus...*") await asyncio.gather(*tasks) # Nochmal KI fragen für Zusammenfassung (optional) summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt(), chat_histories["private"]) await websocket.send_text(f"--- Info ---\n{summary}") except WebSocketDisconnect: pass except Exception as e: print(f"Webchat Fehler: {e}") async def run_remote_task(ip, user, cmd, history_key="private"): # history_key als Parameter await manager.broadcast(f"🚀 Task: {cmd} auf {ip}") proc = await asyncio.create_subprocess_shell( f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {user}@{ip} '{cmd}'", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT ) full_output = "" while True: line = await proc.stdout.readline() if not line: break out = line.decode('utf-8', errors='ignore').strip() if out: await manager.broadcast(f"🛠️ {out}") full_output += out + "\n" await proc.wait() now = datetime.now().strftime("%d.%m.%Y %H:%M") # Jetzt wird in die richtige Historie geschrieben! if history_key in chat_histories: chat_histories[history_key].append({ "role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {ip} fertig:\n{full_output or 'Kein Output'}", "timestamp": now }) # --- Settings API --- @app.get("/api/settings") async def get_settings(): return { "provider": AI_PROVIDER, "google_model": GOOGLE_MODEL, "openai_model": OPENAI_MODEL, "nvidia_model": NVIDIA_MODEL, "ollama_model": OLLAMA_MODEL, "ollama_base_url": OLLAMA_BASE_URL # URL ans Frontend schicken } @app.post("/api/settings") async def update_settings(request: Request): # WICHTIG: OLLAMA_BASE_URL als global deklarieren global AI_PROVIDER, GOOGLE_MODEL, OPENAI_MODEL, NVIDIA_MODEL, OLLAMA_MODEL, OLLAMA_BASE_URL data = await request.json() provider = data.get("provider") model = data.get("model") ollama_url = data.get("ollama_base_url") # URL vom Frontend empfangen if provider: AI_PROVIDER = provider set_key(ENV_FILE, "AI_PROVIDER", provider) if provider == "google" and model: GOOGLE_MODEL = model set_key(ENV_FILE, "GOOGLE_MODEL", model) elif provider == "openai" and model: OPENAI_MODEL = model set_key(ENV_FILE, "OPENAI_MODEL", model) elif provider == "nvidia" and model: NVIDIA_MODEL = model set_key(ENV_FILE, "NVIDIA_MODEL", model) elif provider == "ollama" and model: OLLAMA_MODEL = model set_key(ENV_FILE, "OLLAMA_MODEL", model) # Wenn eine Ollama-URL mitgeschickt wurde, speichern wir sie if ollama_url: OLLAMA_BASE_URL = ollama_url set_key(ENV_FILE, "OLLAMA_BASE_URL", ollama_url) return {"status": "success"} @app.get("/api/models") async def get_models(provider: str, url: str = None): try: models = [] if provider == "ollama" and url: # Das Backend hat keine CORS-Probleme und fragt Ollama direkt clean_url = url.replace("/v1", "").rstrip("/") async with httpx.AsyncClient() as client: response = await client.get(f"{clean_url}/api/tags", timeout=5.0) data = response.json() models = [m["name"] for m in data.get("models", [])] elif provider == "openai": if not OPENAI_API_KEY or "hier" in OPENAI_API_KEY: return {"models": []} # Hier greift das Backend auf deinen OpenAI API-Key zu client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY) response = await client.models.list() # Nur GPT-Modelle filtern, um die Liste sauber zu halten models = [m.id for m in response.data if "gpt" in m.id or "o1" in m.id] models.sort() elif provider == "nvidia": if not NVIDIA_API_KEY or "hier" in NVIDIA_API_KEY: return {"models": ["FEHLER: Key fehlt in .env"]} client = openai.AsyncOpenAI( api_key=NVIDIA_API_KEY, base_url="https://integrate.api.nvidia.com/v1" ) try: # Timeout hinzufügen, damit die Seite nicht ewig hängt response = await asyncio.wait_for(client.models.list(), timeout=5.0) models = [m.id for m in response.data] models.sort() return {"models": models} except Exception as e: print(f"NVIDIA API Error: {e}") return {"models": [f"NVIDIA Fehler: {str(e)[:20]}..."]} elif provider == "google": if not GOOGLE_API_KEY: return {"models": ["API-Key fehlt"]} client = genai.Client(api_key=GOOGLE_API_KEY) models = [] # Im neuen SDK (google-genai) heißt das Feld 'supported_actions' for m in client.models.list(): if 'generateContent' in m.supported_actions: # Wir nehmen den Namen und entfernen das 'models/' Präfix model_name = m.name.replace("models/", "") models.append(model_name) models.sort() return {"models": models} except Exception as e: print(f"Fehler beim Abrufen der Modelle für {provider}: {str(e)}") return {"models": []} # Gibt eine leere Liste zurück -> Frontend nutzt Fallback @app.get("/debug_keys") async def debug_keys(): return { "AI_PROVIDER": AI_PROVIDER, "NVIDIA_KEY_LOADED": bool(NVIDIA_API_KEY and "hier" not in NVIDIA_API_KEY), "NVIDIA_KEY_START": NVIDIA_API_KEY[:10] if NVIDIA_API_KEY else "Missing", "GOOGLE_KEY_LOADED": bool(GOOGLE_API_KEY), "OLLAMA_URL": OLLAMA_BASE_URL } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)