Files
PiDoBot/source/main.py

747 lines
29 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import pty
import fcntl
import subprocess
import sqlite3
import asyncio
import openai
import re
import httpx
import struct
import termios
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"
# 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")
chat_history = []
# 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():
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"
workspace_context = f"""
### DEIN WORKSPACE (LOKALER SERVER)
Du hast Zugriff auf ein eigenes Arbeitsverzeichnis für Notizen und Aufgaben:
- Pfad: {WORKSPACE_DIR}
- Notizen: {NOTES_FILE}
- Aufgaben: {TODO_FILE}
Du kannst diese Dateien jederzeit lesen oder beschreiben, um dir Dinge für Tony zu merken.
Nutze dazu: <EXECUTE target="localhost">Befehl</EXECUTE>
"""
# Hier liest du die system_prompt.txt aus config/
if PROMPT_FILE.exists():
template = PROMPT_FILE.read_text(encoding="utf-8")
else:
template = "Du bist J.A.R.V.I.S... {workspace_context}\nNodes: {node_info}"
prompt = template.replace("{node_info}", node_info)
prompt = prompt.replace("{workspace_context}", workspace_context)
return prompt
# --- KI FUNKTIONEN ---
async def get_ai_response(user_input, system_prompt):
global chat_history
chat_history.append({"role": "user", "content": user_input})
chat_history = chat_history[-30:]
ai_msg = ""
try:
# Kombinierte Logik für OpenAI, Ollama und NVIDIA (alle nutzen das OpenAI SDK)
if AI_PROVIDER in ["openai", "ollama", "nvidia"]:
messages = [{"role": "system", "content": system_prompt}] + chat_history
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
# WICHTIG: Hier .AsyncOpenAI nutzen, da die Funktion async ist
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":
# Für Google Gemini
if not GOOGLE_API_KEY:
return "Fehler: GOOGLE_API_KEY fehlt in der .env Datei!"
client = genai.Client(api_key=GOOGLE_API_KEY)
# Wir müssen unser Array in das spezielle Google-Format umwandeln
google_history = []
# Alle Nachrichten AUSSER der allerletzten (die aktuelle User-Frage) in die History packen
for msg in chat_history[:-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 MIT dem übersetzten Gedächtnis starten
chat = client.chats.create(
model=GOOGLE_MODEL,
config=types.GenerateContentConfig(system_instruction=system_prompt),
history=google_history
)
# Jetzt erst die neue Nachricht an den Chat mit Gedächtnis schicken
response = chat.send_message(user_input)
ai_msg = response.text
except Exception as e:
ai_msg = f"Fehler bei der KI-Anfrage: {e}"
print(f"KI Fehler: {e}")
# 3. Die Antwort der KI ebenfalls ins Gedächtnis aufnehmen
chat_history.append({"role": "assistant", "content": ai_msg})
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)
# 1. Gruppen-Logik: Nur reagieren, wenn der Bot @erwähnt wird
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, damit die KI nicht verwirrt wird
user_msg = user_msg.replace(bot_username, "").strip()
else:
# Im Einzelchat kann optional weiterhin nur der Admin zugelassen werden.
# Wenn du willst, dass auch andere den Bot privat nutzen können, entferne diesen Block:
if user_id != ALLOWED_ID:
await update.message.reply_text("Zugriff auf den privaten Chat verweigert. 🔒")
return
# Tipp-Status anzeigen
await update.message.reply_chat_action(action="typing")
# 2. KI fragen
ai_response = await get_ai_response(user_msg, get_system_prompt())
commands = re.findall(r'<EXECUTE target="(.*?)">(.*?)</EXECUTE>', ai_response, re.I | re.S)
clean_msg = re.sub(r'<EXECUTE.*?>.*?</EXECUTE>', '', ai_response, flags=re.I | re.S).strip()
# 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')
chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {target} fertig:\n{result_text}"})
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.")
# --- 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()
try:
while True:
user_msg = await websocket.receive_text()
ai_response = await get_ai_response(user_msg, get_system_prompt())
commands = re.findall(r'<EXECUTE target="(.*?)">(.*?)</EXECUTE>', ai_response, re.I | re.S)
clean_msg = re.sub(r'<EXECUTE.*?>.*?</EXECUTE>', '', ai_response, flags=re.I | re.S).strip()
if clean_msg: await websocket.send_text(clean_msg)
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: tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip()))
if tasks:
await websocket.send_text(" *Führe Befehle aus...*")
await asyncio.gather(*tasks)
summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt())
await websocket.send_text(f"--- Info ---\n{summary}")
except: pass
async def run_remote_task(ip, user, cmd):
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()
chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {ip} fertig:\n{full_output or 'Kein Output'}"})
# --- 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)