1147 lines
34 KiB
Bash
1147 lines
34 KiB
Bash
#!/bin/bash
|
||
|
||
# Abbrechen bei Fehlern
|
||
set -e
|
||
|
||
echo "===================================================="
|
||
echo "🚀 Starte J.A.R.V.I.S. Desktop OS - Pure Local Setup"
|
||
echo "===================================================="
|
||
|
||
# Benutzererkennung
|
||
REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || whoami)}"
|
||
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
|
||
|
||
if [ -z "$REAL_USER" ] || [ "$REAL_USER" = "root" ]; then
|
||
REAL_USER=$(id -nu 1000 2>/dev/null || echo "meik")
|
||
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
|
||
fi
|
||
|
||
JARVIS_DIR="$REAL_HOME/jarvis-ai"
|
||
|
||
# 1. System aktualisieren & Basispakete installieren
|
||
echo "📦 Aktualisiere Paketquellen und installiere Systemkomponenten..."
|
||
sudo apt update
|
||
sudo apt install -y labwc firefox-esr curl wget git sudo python3 python3-pip python3-venv original-awk tilix geany waybar wlr-randr fonts-noto-color-emoji wofi pipewire pipewire-audio-client-libraries pipewire-pulse wireplumber alsa-utils
|
||
|
||
# 1.1 Gruppenrechte für Grafik und Eingabe
|
||
echo "👥 Füge Benutzer '$REAL_USER' zu den Grafik- und Input-Gruppen hinzu..."
|
||
sudo usermod -aG video,render,input "$REAL_USER"
|
||
systemctl --user --now enable pipewire.service pipewire-pulse.service wireplumber.service
|
||
|
||
# 2. Architektur erkennen und wdotool installieren
|
||
ARCH=$(uname -m)
|
||
echo "🔍 Erkannte Systemarchitektur: $ARCH"
|
||
|
||
if [ "$ARCH" = "x86_64" ]; then
|
||
wget https://github.com/cushycush/wdotool/releases/download/v0.5.3/wdotool_0.5.3-1_amd64.deb -O /tmp/wdotool.deb
|
||
sudo apt install -y /tmp/wdotool.deb
|
||
rm /tmp/wdotool.deb
|
||
elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
||
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cushycush/wdotool/releases/download/v0.5.3/wdotool-installer.sh | sh
|
||
else
|
||
echo "❌ Unbekannte Architektur: $ARCH. Installation abgebrochen."
|
||
exit 1
|
||
fi
|
||
|
||
# 3. Den realen Installationspfad von wdotool ermitteln
|
||
echo "🛠️ Konfiguriere globalen wdotool-Wrapper..."
|
||
if [ -f "/usr/bin/wdotool" ]; then REAL_WDOTOOL="/usr/bin/wdotool"
|
||
elif [ -f "$REAL_HOME/.cargo/bin/wdotool" ]; then REAL_WDOTOOL="$REAL_HOME/.cargo/bin/wdotool"
|
||
elif [ -f "$REAL_HOME/.local/bin/wdotool" ]; then REAL_WDOTOOL="$REAL_HOME/.local/bin/wdotool"
|
||
else REAL_WDOTOOL=$(which wdotool || true); fi
|
||
|
||
if [ -z "$REAL_WDOTOOL" ]; then echo "❌ Fehler: wdotool nicht gefunden!"; exit 1; fi
|
||
|
||
sudo mv "$REAL_WDOTOOL" /usr/local/bin/wdotool.real
|
||
|
||
# 4.1 wdotool Wrapper erstellen
|
||
sudo cat << 'EOF' | sudo tee /usr/local/bin/wdotool > /dev/null
|
||
#!/bin/bash
|
||
export XDG_RUNTIME_DIR=/run/user/$(id -u)
|
||
export WAYLAND_DISPLAY=wayland-0
|
||
exec /usr/local/bin/wdotool.real "$@"
|
||
EOF
|
||
sudo chmod +x /usr/local/bin/wdotool
|
||
|
||
# 4.2 wdotool Wrapper erstellen
|
||
sudo cat << 'EOF' | sudo tee /usr/local/bin/jwin > /dev/null
|
||
#!/bin/bash
|
||
|
||
ACTION=$1
|
||
APP_NAME=$2
|
||
|
||
if [ -z "$ACTION" ] || [ -z "$APP_NAME" ]; then
|
||
echo "❌ Fehler: Falsche Syntax."
|
||
echo "Nutzung: jwin <start|activate|close|snap|maximize> <Programm/Fenstername> [Parameter]"
|
||
exit 1
|
||
fi
|
||
|
||
# =========================================================
|
||
# 1. SONDERFALL: PROGRAMM STARTEN (Mit Display-Erkennung)
|
||
# =========================================================
|
||
if [ "$ACTION" == "start" ]; then
|
||
shift
|
||
|
||
# Falls WAYLAND_DISPLAY nicht gesetzt ist (z.B. im JARVIS-Dienst),
|
||
# versuchen wir es automatisch zu erraten (meistens wayland-0)
|
||
if [ -z "$WAYLAND_DISPLAY" ]; then
|
||
export WAYLAND_DISPLAY=$(ls /run/user/$(id -u)/wayland-* 2>/dev/null | head -n 1 | xargs basename)
|
||
# Fallback auf Standard, falls obiges leer ist
|
||
[ -z "$WAYLAND_DISPLAY" ] && export WAYLAND_DISPLAY="wayland-0"
|
||
fi
|
||
|
||
# Sicherheits-Fallback für ältere X11/XWayland Apps wie Geany
|
||
if [ -z "$DISPLAY" ]; then
|
||
export DISPLAY=":0"
|
||
fi
|
||
|
||
# Das Programm mit den exportierten Variablen starten
|
||
nohup "$@" >/dev/null 2>&1 &
|
||
|
||
echo "✅ Programm '$APP_NAME' wurde entkoppelt im Hintergrund gestartet (Display: $WAYLAND_DISPLAY)."
|
||
exit 0
|
||
fi
|
||
|
||
# =========================================================
|
||
# 2. WAYLAND AKTIONEN (Fokus-optimiert)
|
||
# =========================================================
|
||
PARAM1=$3
|
||
WD="wdotool --backend wlr-protocols"
|
||
|
||
if [ "$APP_NAME" == "active" ]; then
|
||
# Absolut kugelsicher: Nimm einfach das Fenster, das GERADE aktiv ist
|
||
WINDOW_ID=$($WD getactivewindow 2>/dev/null | awk '{print $1}')
|
||
else
|
||
# Normale Suche für gezielte Befehle (wie "schließe geany")
|
||
WINDOW_LINE=$($WD search --ignore-case --any --name "$APP_NAME" --class "$APP_NAME" 2>/dev/null | head -n 1)
|
||
|
||
if [ -z "$WINDOW_LINE" ]; then
|
||
SAFE_APP_NAME=$(echo "$APP_NAME" | sed 's/[.[\*^$]/\\&/g')
|
||
WINDOW_LINE=$($WD search --ignore-case --regex --any --name "$SAFE_APP_NAME" --class "$SAFE_APP_NAME" 2>/dev/null | head -n 1)
|
||
fi
|
||
WINDOW_ID=$(echo "$WINDOW_LINE" | awk '{print $1}')
|
||
fi
|
||
|
||
if [ -z "$WINDOW_ID" ]; then
|
||
echo "❌ Kein aktives oder passendes Fenster gefunden."
|
||
exit 1
|
||
fi
|
||
|
||
case "$ACTION" in
|
||
activate)
|
||
$WD windowactivate "$WINDOW_ID"
|
||
echo "✅ Fenster '$APP_NAME' (ID: $WINDOW_ID) ist jetzt im Fokus."
|
||
;;
|
||
close)
|
||
$WD windowclose "$WINDOW_ID"
|
||
echo "✅ Fenster '$APP_NAME' (ID: $WINDOW_ID) wurde geschlossen."
|
||
;;
|
||
maximize)
|
||
# 1. Fenster aktivieren
|
||
$WD windowactivate "$WINDOW_ID"
|
||
sleep 0.2
|
||
# 2. Labwc Vollbild-Shortcut senden (oft Super+Up oder Alt+F11, passe das an deine rc.xml an!)
|
||
$WD key super+up
|
||
echo "✅ Fenster '$APP_NAME' maximiert."
|
||
;;
|
||
snap)
|
||
if [ -z "$PARAM1" ]; then
|
||
echo "❌ Fehler: Für 'snap' wird eine Richtung (left, right, up, down) benötigt."
|
||
exit 1
|
||
fi
|
||
|
||
# 1. Fenster MUSS zuerst fokussiert werden (Wayland-Sicherheitsregel für Input)
|
||
$WD windowactivate "$WINDOW_ID"
|
||
sleep 0.2 # Kurze Pause, damit der Fokus greift
|
||
|
||
# 2. Sende die Tastenkombination an labwc zum Andocken
|
||
case "$PARAM1" in
|
||
left) $WD key super+Left ;;
|
||
right) $WD key super+Right ;;
|
||
up) $WD key super+Up ;;
|
||
down) $WD key super+Down ;;
|
||
# --- ECKEN (Super + Shift + Pfeiltasten für maximale Kompatibilität) ---
|
||
top-left) $WD key super+shift+Left ;;
|
||
top-right) $WD key super+shift+Up ;;
|
||
bottom-right) $WD key super+shift+Down ;;
|
||
bottom-left) $WD key super+shift+Right ;;
|
||
*) echo "❌ Unbekannte Richtung. Nutze left, right, up, down." ; exit 1 ;;
|
||
esac
|
||
if [ $? -eq 0 ]; then
|
||
echo "✅ Fenster '$APP_NAME' nach $PARAM1 angedockt."
|
||
else
|
||
echo "❌ Fehler beim Senden der Tastenkombination an wdotool."
|
||
exit 1
|
||
fi
|
||
;;
|
||
*)
|
||
echo "❌ Unbekannte Aktion: $ACTION. Erlaubt sind: start, activate, close, snap, maximize."
|
||
exit 1
|
||
;;
|
||
esac
|
||
EOF
|
||
sudo chmod +x /usr/local/bin/jwin
|
||
|
||
|
||
# 5. Desktop-Konfiguration (labwc & environment)
|
||
echo "📂 Konfiguriere labwcAutostart und Tastaturlayout..."
|
||
mkdir -p "$REAL_HOME/.config/labwc"
|
||
|
||
cat << 'EOF' > "$REAL_HOME/.config/labwc/environment"
|
||
dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=labwc
|
||
XKB_DEFAULT_LAYOUT=de
|
||
EOF
|
||
|
||
# 6. Auto-Start in .profile eintragen
|
||
echo "⚙️ Richte .profile für Wayland Auto-Start ein..."
|
||
if ! grep -q "labwc" "$REAL_HOME/.profile"; then
|
||
cat << 'EOF' >> "$REAL_HOME/.profile"
|
||
|
||
if [ "$(tty)" = "/dev/tty1" ]; then
|
||
export WLR_RENDERER=pixman
|
||
export WLR_NO_HARDWARE_CURSORS=1
|
||
export LIBGL_ALWAYS_SOFTWARE=1
|
||
dbus-run-session labwc > ~/labwc.log 2>&1
|
||
logout
|
||
fi
|
||
EOF
|
||
fi
|
||
|
||
# 7. Waybar & Wofi Powermenu
|
||
echo "📊 Konfiguriere Waybar und Wofi Power-Menü..."
|
||
mkdir -p "$REAL_HOME/.config/waybar"
|
||
|
||
cat << 'EOF' > "$REAL_HOME/.config/waybar/config"
|
||
{
|
||
"layer": "top", "position": "bottom", "height": 34,
|
||
"modules-left": ["custom/terminal", "custom/browser", "wlr/taskbar"],
|
||
"modules-right": ["clock", "custom/logout"],
|
||
"custom/terminal": { "format": "📁 Term", "on-click": "tilix", "tooltip": false },
|
||
"custom/browser": { "format": "🌐 Web", "on-click": "MOZ_WEBRENDER=software MOZ_ENABLE_WAYLAND=1 firefox-esr", "tooltip": false },
|
||
"wlr/taskbar": { "format": "{icon}", "icon-size": 16, "on-click": "activate", "on-click-right": "minimize" },
|
||
"clock": { "format": "🕒 {:%H:%M:%S}", "interval": 1 },
|
||
"custom/logout": { "format": "⚙️ System ", "on-click": "~/.config/labwc/powermenu.sh", "tooltip": false }
|
||
}
|
||
EOF
|
||
|
||
cat << 'EOF' > "$REAL_HOME/.config/waybar/style.css"
|
||
* { font-family: sans-serif; font-size: 12px; }
|
||
window#waybar { background-color: #1e293b; color: white; border-top: 1px solid #334155; }
|
||
#custom-terminal, #custom-browser { padding: 0 8px; background: #334155; margin: 3px 2px; border-radius: 3px; }
|
||
#custom-terminal:hover, #custom-browser:hover { background: #475569; }
|
||
#taskbar button { padding: 0 10px; color: #94a3b8; }
|
||
#taskbar button.active { background-color: #0f172a; color: #38bdf8; }
|
||
#clock { padding: 0 15px; background-color: #0f172a; }
|
||
#custom-logout { padding: 0 12px; background-color: #ef4444; color: white; }
|
||
#custom-logout:hover { background-color: #dc2626; }
|
||
EOF
|
||
|
||
cat << 'EOF' > "$REAL_HOME/.config/labwc/powermenu.sh"
|
||
#!/bin/bash
|
||
OPTIONS="🚪 Abmelden\n🔄 Neu starten\n🛑 Herunterfahren"
|
||
CHOICE=$(echo -e "$OPTIONS" | wofi --dmenu --prompt "Systemaktion wählen:" --width 280 --height 180 --style "$HOME/.config/labwc/wofi-power.css")
|
||
case "$CHOICE" in
|
||
*"Abmelden") labwc --exit ;;
|
||
*"Neu starten") sudo systemctl reboot ;;
|
||
*"Herunterfahren") sudo systemctl poweroff ;;
|
||
esac
|
||
EOF
|
||
chmod +x "$REAL_HOME/.config/labwc/powermenu.sh"
|
||
|
||
cat << 'EOF' > "$REAL_HOME/.config/labwc/wofi-power.css"
|
||
window { background-color: #1e293b; color: white; border: 2px solid #334155; border-radius: 8px; font-family: sans-serif; }
|
||
#entry { padding: 8px; color: white; }
|
||
#entry:selected { background-color: #334155; color: #38bdf8; }
|
||
#input { background-color: #0f172a; color: white; border: 1px solid #334155; margin: 5px; }
|
||
EOF
|
||
|
||
echo "%sudo ALL=(ALL) NOPASSWD: /usr/bin/systemctl poweroff, /usr/bin/systemctl reboot" | sudo tee /etc/sudoers.d/powermenu > /dev/null
|
||
sudo chmod 0440 /etc/sudoers.d/powermenu
|
||
|
||
|
||
# ===================================================================
|
||
# 8. J.A.R.V.I.S. REIN LOKALE INSTALLATION
|
||
# ===================================================================
|
||
echo "🧠 Installiere J.A.R.V.I.S. AI Desktop Backend..."
|
||
mkdir -p "$JARVIS_DIR"/{config,workspace}
|
||
|
||
# J.A.R.V.I.S. Requirements
|
||
cat << 'EOF' > "$JARVIS_DIR/requirements.txt"
|
||
openai
|
||
google-genai
|
||
python-dotenv
|
||
EOF
|
||
|
||
# J.A.R.V.I.S. .env Template
|
||
cat << 'EOF' > "$JARVIS_DIR/config/.env"
|
||
WEB_USER_NAME=$REAL_USER
|
||
AI_PROVIDER=nvdia
|
||
OPENAI_API_KEY=dein-openai-key
|
||
OPENAI_MODEL=
|
||
GOOGLE_API_KEY=dein-google-key
|
||
NVIDIA_API_KEY=dein-nvidia-key
|
||
NVIDIA_MODEL=moonshotai/kimi-k2.6
|
||
GOOGLE_MODEL=gemini-2.5-flash
|
||
OLLAMA_BASE_URL=http://127.0.0.1:11434/v1
|
||
OLLAMA_MODEL=llama3
|
||
GROQ_API_KEY=dein-groq-key
|
||
GROQ_MODEL=groq/compound
|
||
EOF
|
||
|
||
# J.A.R.V.I.S. Optimierter System-Prompt inkl. wdotool-Handbuch
|
||
cat << 'EOF' > "$JARVIS_DIR/config/system_prompt.txt"
|
||
Du bist J.A.R.V.I.S., ein KI-Systemassistent, der direkt auf einem Debian Wayland-Desktop läuft. Du hast vollen lokalen Zugriff auf das System.
|
||
|
||
UMGEBUNG & GEDÄCHTNIS:
|
||
- Arbeitsverzeichnis: {workspace_dir}
|
||
- Notizen: {notes_file}
|
||
- Todos: {todo_file}
|
||
|
||
DESKTOP STEUERUNG & FENSTER-MANAGEMENT:
|
||
Du steuerst die grafische Oberfläche (Wayland/labwc) über Befehlszeilen-Tools.
|
||
|
||
1. Programme & Fenster verwalten (Tool: jwin)
|
||
Du kannst Fenster steuern, indem du das jwin-Skript aufrufst.
|
||
Folgende Aktionen sind erlaubt:
|
||
- start (Startet ein Programm)
|
||
- activate (Holt ein Fenster in den Vordergrund)
|
||
- close (Schließt das Fenster)
|
||
- snap (Dockt das Fenster an. Parameter: left, right, up, down, top-left, bottom-left, top-right, bottom-right)
|
||
- maximize (Maximiert das Fenster)
|
||
Beispiele:
|
||
<EXECUTE>jwin snap firefox right</EXECUTE> (Schiebt Firefox auf die rechte Bildschirmhälfte)
|
||
<EXECUTE>jwin snap firefox bottom-right</EXECUTE> (Schiebt Firefox auf die rechte untere Bildschirmviertel)
|
||
<EXECUTE>jwin activate terminal</EXECUTE> (Holt das Terminal in den Vordergrund)
|
||
|
||
Regel für Multitasking-Fenster: Wenn der User mehrere Instanzen derselben App starten und verteilen möchte, führe die Befehle immer abwechselnd aus und nutze das Schlüsselwort active für das Snapping:
|
||
Beispiel-Verkettung für das System:
|
||
<EXECUTE>jwin start firefox && sleep 1.5 && jwin snap active top-left && jwin start firefox && sleep 1.5 && jwin snap active top-right</EXECUTE>
|
||
|
||
2. Tastatur & Maus (Tool: wdotool)
|
||
- Tastatur: wdotool key ctrl+l, wdotool key alt+Tab, wdotool type "Hallo"
|
||
- Maus: wdotool mousemove 500 400 (absolut), wdotool click 1 (1=links, 3=rechts)
|
||
|
||
3. System- & Display-Infos
|
||
- Bildschirmauflösung ermitteln: <EXECUTE>wlr-randr | grep current | awk '{print $1}'</EXECUTE>
|
||
|
||
WICHTIGE REGELN FÜR DIE AUSFÜHRUNG:
|
||
1. Wenn du eine Aktion ausführst, MUSST du den Linux-Befehl EXAKT in <EXECUTE> und </EXECUTE> Tags setzen.
|
||
2. VERBOTEN: Verwende NIEMALS Markdown-Codeblöcke (```) um oder in den <EXECUTE>-Tags! Schreibe die Tags als simplen, rohen Text.
|
||
3. ERZWUNGEN: Sag nicht nur, dass du etwas tust – du MUSST den <EXECUTE> Tag in deiner Antwort mitsenden, sonst passiert nichts!
|
||
|
||
Beispiel für einen perfekten Workflow:
|
||
Das mache ich sofort für dich!
|
||
<EXECUTE>jwin activate Firefox && sleep 1 && jwin move Firefox 0 0</EXECUTE>
|
||
|
||
Schreibe immer eine kurze Textantwort dazu, was du gerade tust. Du duzt {user_name} konsequent, dein Tonfall ist locker und technisch versiert.
|
||
EOF
|
||
|
||
# J.A.R.V.I.S. Python Backend (Kein SQLite, schlanker Regex)
|
||
cat << 'EOF' > "$JARVIS_DIR/jarvis.py"
|
||
import os
|
||
import re
|
||
import sqlite3
|
||
import asyncio
|
||
import openai
|
||
|
||
from google import genai
|
||
from google.genai import types
|
||
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from dotenv import load_dotenv
|
||
|
||
from colorama import init, Fore, Style
|
||
|
||
|
||
# ====================================================
|
||
# INITIALISIERUNG
|
||
# ====================================================
|
||
|
||
init(autoreset=True)
|
||
|
||
|
||
# ====================================================
|
||
# PFADE & SETUP
|
||
# ====================================================
|
||
|
||
BASE_DIR = Path(__file__).resolve().parent
|
||
CONFIG_DIR = BASE_DIR / "config"
|
||
DATA_DIR = BASE_DIR / "data"
|
||
WORKSPACE_DIR = BASE_DIR / "workspace"
|
||
|
||
ENV_FILE = CONFIG_DIR / ".env"
|
||
load_dotenv(ENV_FILE)
|
||
|
||
DB_PATH = DATA_DIR / "cluster.db"
|
||
NOTES_FILE = WORKSPACE_DIR / "NOTIZEN.md"
|
||
TODO_FILE = WORKSPACE_DIR / "TODO.md"
|
||
CHAT_LOG_FILE = WORKSPACE_DIR / "chat_history.log"
|
||
|
||
WEB_USER_NAME = os.getenv("WEB_USER_NAME", "Meik")
|
||
|
||
|
||
# ====================================================
|
||
# TERMINAL FARBEN
|
||
# ====================================================
|
||
|
||
USER_COLOR = Fore.CYAN
|
||
JARVIS_COLOR = Fore.GREEN
|
||
SYSTEM_COLOR = Fore.YELLOW
|
||
ERROR_COLOR = Fore.RED
|
||
OUTPUT_COLOR = Fore.MAGENTA
|
||
INFO_COLOR = Fore.BLUE
|
||
|
||
RESET = Style.RESET_ALL
|
||
|
||
|
||
# ====================================================
|
||
# ORDNER & DATEIEN
|
||
# ====================================================
|
||
|
||
for d in [WORKSPACE_DIR, DATA_DIR, CONFIG_DIR]:
|
||
d.mkdir(parents=True, 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"
|
||
)
|
||
|
||
|
||
# ====================================================
|
||
# 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", "")
|
||
GROQ_API_KEY = os.getenv("GROQ_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.5-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.6"
|
||
)
|
||
|
||
GROQ_MODEL = os.getenv(
|
||
"GROQ_MODEL",
|
||
"meta-llama/llama-4-scout-17b-16e-instruct"
|
||
)
|
||
|
||
# ====================================================
|
||
# DATENBANK
|
||
# ====================================================
|
||
|
||
def init_db():
|
||
conn = sqlite3.connect(DB_PATH)
|
||
|
||
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
|
||
|
||
|
||
# ====================================================
|
||
# SYSTEM PROMPT
|
||
# ====================================================
|
||
|
||
def get_system_prompt():
|
||
|
||
prompt_path = CONFIG_DIR / "system_prompt.txt"
|
||
|
||
prompt = (
|
||
prompt_path.read_text(encoding="utf-8")
|
||
if prompt_path.exists()
|
||
else f"Hallo {WEB_USER_NAME}, ich bin J.A.R.V.I.S."
|
||
)
|
||
|
||
prompt = prompt.replace("{user_name}", WEB_USER_NAME)
|
||
prompt = prompt.replace("{workspace_dir}", str(WORKSPACE_DIR))
|
||
prompt = prompt.replace("{notes_file}", str(NOTES_FILE))
|
||
prompt = prompt.replace("{todo_file}", str(TODO_FILE))
|
||
|
||
conn = get_db()
|
||
|
||
nodes = conn.execute(
|
||
'SELECT * FROM nodes'
|
||
).fetchall()
|
||
|
||
conn.close()
|
||
|
||
node_info = ""
|
||
|
||
for n in nodes:
|
||
node_info += (
|
||
f"- Name: {n['name']}, "
|
||
f"IP: {n['ip']}, "
|
||
f"User: {n['user']}\n"
|
||
)
|
||
|
||
return prompt.replace("{node_info}", node_info)
|
||
|
||
|
||
# ====================================================
|
||
# KI KOMMUNIKATION
|
||
# ====================================================
|
||
|
||
async def get_ai_response(user_msg, system_prompt, history_list):
|
||
|
||
try:
|
||
|
||
if AI_PROVIDER in ["openai", "ollama", "nvidia", "groq"]:
|
||
|
||
# ========================================
|
||
# PAYLOAD WASCHEN (Für strikte APIs wie Groq)
|
||
# ========================================
|
||
clean_history = [
|
||
{
|
||
"role": msg["role"],
|
||
"content": msg["content"]
|
||
}
|
||
for msg in history_list
|
||
]
|
||
|
||
messages = [
|
||
{
|
||
"role": "system",
|
||
"content": system_prompt
|
||
}
|
||
] + clean_history
|
||
|
||
if AI_PROVIDER == "ollama":
|
||
|
||
url = (
|
||
OLLAMA_BASE_URL
|
||
if OLLAMA_BASE_URL.endswith('/v1')
|
||
else OLLAMA_BASE_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
|
||
|
||
elif AI_PROVIDER == "groq":
|
||
url = "https://api.groq.com/openai/v1"
|
||
key = GROQ_API_KEY
|
||
model_to_use = GROQ_MODEL
|
||
|
||
else:
|
||
|
||
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
|
||
)
|
||
|
||
return response.choices[0].message.content
|
||
|
||
elif AI_PROVIDER == "google":
|
||
|
||
client = genai.Client(api_key=GOOGLE_API_KEY)
|
||
|
||
google_history = [
|
||
types.Content(
|
||
role="user" if msg["role"] == "user" else "model",
|
||
parts=[
|
||
types.Part.from_text(
|
||
text=msg["content"]
|
||
)
|
||
]
|
||
)
|
||
for msg in history_list[:-1]
|
||
]
|
||
|
||
chat = client.chats.create(
|
||
model=GOOGLE_MODEL,
|
||
config=types.GenerateContentConfig(
|
||
system_instruction=system_prompt
|
||
),
|
||
history=google_history
|
||
)
|
||
|
||
return chat.send_message(user_msg).text
|
||
|
||
except Exception as e:
|
||
|
||
return f"Fehler bei der KI-Anfrage: {e}"
|
||
|
||
|
||
# ====================================================
|
||
# BEFEHLSAUSFÜHRUNG
|
||
# ====================================================
|
||
|
||
async def run_task(target, cmd):
|
||
|
||
print(
|
||
f"\n{SYSTEM_COLOR}"
|
||
f"⚙️ STARTE TASK AUF [{target}]"
|
||
f"{RESET}"
|
||
)
|
||
|
||
print(
|
||
f"{INFO_COLOR}"
|
||
f"➡️ {cmd}"
|
||
f"{RESET}\n"
|
||
)
|
||
|
||
try:
|
||
|
||
# ========================================
|
||
# GUI APPS ERKENNEN
|
||
# ========================================
|
||
|
||
gui_apps = [
|
||
"firefox",
|
||
"thunderbird",
|
||
"chromium",
|
||
"google-chrome",
|
||
"code",
|
||
"nautilus",
|
||
"pcmanfm",
|
||
"gedit",
|
||
"vlc",
|
||
"discord",
|
||
"steam",
|
||
"obs",
|
||
"spotify"
|
||
]
|
||
|
||
first_word = cmd.strip().split()[0]
|
||
|
||
is_gui_app = (
|
||
first_word in gui_apps
|
||
or cmd.strip().endswith("&")
|
||
)
|
||
|
||
# ========================================
|
||
# GUI APPS DETACHED STARTEN
|
||
# ========================================
|
||
|
||
if is_gui_app:
|
||
|
||
detached_cmd = (
|
||
f"nohup {cmd.replace('&', '').strip()} "
|
||
f">/dev/null 2>&1 &"
|
||
)
|
||
|
||
print(
|
||
f"{SYSTEM_COLOR}"
|
||
f"🖥️ GUI-APP erkannt → Detached Mode"
|
||
f"{RESET}"
|
||
)
|
||
|
||
if target.lower() in ["localhost", "127.0.0.1"]:
|
||
|
||
proc = await asyncio.create_subprocess_shell(
|
||
detached_cmd
|
||
)
|
||
|
||
else:
|
||
|
||
conn = get_db()
|
||
|
||
n = conn.execute(
|
||
'SELECT * FROM nodes WHERE ip=? OR name=?',
|
||
(target, target)
|
||
).fetchone()
|
||
|
||
conn.close()
|
||
|
||
if not n:
|
||
|
||
err = (
|
||
f"Node '{target}' "
|
||
f"nicht gefunden."
|
||
)
|
||
|
||
print(
|
||
f"{ERROR_COLOR}{err}{RESET}"
|
||
)
|
||
|
||
return err
|
||
|
||
ssh_cmd = (
|
||
f"ssh "
|
||
f"-o StrictHostKeyChecking=no "
|
||
f"-o LogLevel=ERROR "
|
||
f"{n['user']}@{n['ip']} "
|
||
f"'{detached_cmd}'"
|
||
)
|
||
|
||
proc = await asyncio.create_subprocess_shell(
|
||
ssh_cmd
|
||
)
|
||
|
||
await proc.wait()
|
||
|
||
print(
|
||
f"{JARVIS_COLOR}"
|
||
f"✅ GUI-Programm gestartet"
|
||
f"{RESET}\n"
|
||
)
|
||
|
||
return "GUI application started."
|
||
|
||
# ========================================
|
||
# NORMALE COMMANDS
|
||
# ========================================
|
||
|
||
else:
|
||
|
||
if target.lower() in ["localhost", "127.0.0.1"]:
|
||
|
||
final_cmd = cmd
|
||
|
||
else:
|
||
|
||
conn = get_db()
|
||
|
||
n = conn.execute(
|
||
'SELECT * FROM nodes WHERE ip=? OR name=?',
|
||
(target, target)
|
||
).fetchone()
|
||
|
||
conn.close()
|
||
|
||
if not n:
|
||
|
||
err = (
|
||
f"Node '{target}' "
|
||
f"nicht gefunden."
|
||
)
|
||
|
||
print(
|
||
f"{ERROR_COLOR}{err}{RESET}"
|
||
)
|
||
|
||
return err
|
||
|
||
final_cmd = (
|
||
f"ssh "
|
||
f"-o StrictHostKeyChecking=no "
|
||
f"-o LogLevel=ERROR "
|
||
f"{n['user']}@{n['ip']} "
|
||
f"'{cmd}'"
|
||
)
|
||
|
||
proc = await asyncio.create_subprocess_shell(
|
||
final_cmd,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.STDOUT
|
||
)
|
||
|
||
collected_output = []
|
||
|
||
while True:
|
||
|
||
line = await proc.stdout.readline()
|
||
|
||
if not line:
|
||
break
|
||
|
||
decoded = line.decode(
|
||
"utf-8",
|
||
errors="ignore"
|
||
).rstrip()
|
||
|
||
collected_output.append(decoded)
|
||
|
||
print(
|
||
f"{OUTPUT_COLOR}"
|
||
f"│ {decoded}"
|
||
f"{RESET}"
|
||
)
|
||
|
||
await proc.wait()
|
||
|
||
print()
|
||
|
||
if proc.returncode == 0:
|
||
|
||
print(
|
||
f"{JARVIS_COLOR}"
|
||
f"✅ TASK ERFOLGREICH"
|
||
f"{RESET}\n"
|
||
)
|
||
|
||
else:
|
||
|
||
print(
|
||
f"{ERROR_COLOR}"
|
||
f"❌ FEHLER CODE: "
|
||
f"{proc.returncode}"
|
||
f"{RESET}\n"
|
||
)
|
||
|
||
return "\n".join(collected_output)
|
||
|
||
except Exception as e:
|
||
|
||
err = f"❌ Fehler: {e}"
|
||
|
||
print(
|
||
f"{ERROR_COLOR}"
|
||
f"{err}"
|
||
f"{RESET}\n"
|
||
)
|
||
|
||
return err
|
||
|
||
# ====================================================
|
||
# FILE LOGGING
|
||
# ====================================================
|
||
def log_to_file(role, content):
|
||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
try:
|
||
with open(CHAT_LOG_FILE, "a", encoding="utf-8") as f:
|
||
f.write(f"[{now}] {role.upper()}:\n{content}\n{'-'*60}\n")
|
||
except Exception as e:
|
||
print(f"{ERROR_COLOR}⚠️ Konnte nicht ins Log schreiben: {e}{RESET}")
|
||
|
||
# ====================================================
|
||
# USER INPUT
|
||
# ====================================================
|
||
|
||
async def listen_to_user():
|
||
|
||
return await asyncio.to_thread(
|
||
input,
|
||
f"\n{USER_COLOR}👤 Du:{RESET} "
|
||
)
|
||
|
||
|
||
# ====================================================
|
||
# JARVIS OUTPUT
|
||
# ====================================================
|
||
|
||
async def speak_to_user(text):
|
||
|
||
print(
|
||
f"\n{JARVIS_COLOR}"
|
||
f"🤖 J.A.R.V.I.S."
|
||
f"{RESET}"
|
||
)
|
||
|
||
print(
|
||
f"{JARVIS_COLOR}"
|
||
f"{'-'*60}"
|
||
f"{RESET}"
|
||
)
|
||
|
||
print(text)
|
||
|
||
print(
|
||
f"{JARVIS_COLOR}"
|
||
f"{'-'*60}"
|
||
f"{RESET}\n"
|
||
)
|
||
|
||
|
||
# ====================================================
|
||
# MAIN LOOP
|
||
# ====================================================
|
||
|
||
async def main_chat_loop():
|
||
|
||
print(f"{INFO_COLOR}")
|
||
|
||
print("====================================================")
|
||
print("🤖 J.A.R.V.I.S. Terminal Interface geladen")
|
||
print(f"🧠 Provider: {AI_PROVIDER.upper()}")
|
||
print("⌨️ Tippe 'exit', um zu beenden")
|
||
print("====================================================")
|
||
|
||
print(RESET)
|
||
|
||
chat_history = []
|
||
|
||
while True:
|
||
|
||
user_msg = await listen_to_user()
|
||
|
||
if user_msg.lower().strip() in ['exit', 'quit']:
|
||
|
||
print(
|
||
f"{SYSTEM_COLOR}"
|
||
f"\nJ.A.R.V.I.S. geht offline."
|
||
f"{RESET}"
|
||
)
|
||
|
||
break
|
||
|
||
if not user_msg.strip():
|
||
continue
|
||
|
||
now = datetime.now().strftime("%d.%m.%Y %H:%M")
|
||
|
||
chat_history.append({
|
||
"role": "user",
|
||
"content": user_msg,
|
||
"timestamp": now
|
||
})
|
||
|
||
# LOG: User Eingabe hier schreiben!
|
||
log_to_file("Du", user_msg)
|
||
|
||
print(
|
||
f"{SYSTEM_COLOR}"
|
||
f"🧠 J.A.R.V.I.S. denkt nach..."
|
||
f"{RESET}",
|
||
end="\r"
|
||
)
|
||
|
||
system_prompt = get_system_prompt()
|
||
|
||
ai_response = await get_ai_response(
|
||
user_msg,
|
||
system_prompt,
|
||
chat_history
|
||
)
|
||
|
||
# ============================================
|
||
# EXECUTE TAGS SUCHEN
|
||
# Unterstützt:
|
||
# <EXECUTE>cmd</EXECUTE>
|
||
# <EXECUTE target="xyz">cmd</EXECUTE>
|
||
# ============================================
|
||
|
||
commands = []
|
||
|
||
# Toleranter Regex: Erlaubt Leerzeichen vor dem '>'
|
||
execute_matches = re.finditer(
|
||
r'<EXECUTE[^>]*?(?:target="(.*?)")?[^>]*>(.*?)</EXECUTE>',
|
||
ai_response,
|
||
re.I | re.S
|
||
)
|
||
|
||
for match in execute_matches:
|
||
target = match.group(1)
|
||
cmd = match.group(2)
|
||
|
||
if not target:
|
||
target = "localhost"
|
||
|
||
# Markdown-Backticks bereinigen, falls die KI sie in den Tag mogelt
|
||
cmd = cmd.strip()
|
||
cmd = re.sub(r'^```[a-zA-Z]*\n?', '', cmd)
|
||
cmd = re.sub(r'\n?```$', '', cmd)
|
||
cmd = cmd.strip()
|
||
|
||
commands.append((target.strip(), cmd))
|
||
|
||
# Die Tags für die Sprach-/Textausgabe sauber entfernen
|
||
clean_msg = re.sub(
|
||
r'<EXECUTE[^>]*?>.*?</EXECUTE>',
|
||
'',
|
||
ai_response,
|
||
flags=re.I | re.S
|
||
).strip()
|
||
|
||
# ============================================
|
||
# JARVIS TEXT
|
||
# ============================================
|
||
|
||
if clean_msg:
|
||
|
||
await speak_to_user(clean_msg)
|
||
|
||
chat_history.append({
|
||
"role": "assistant",
|
||
"content": clean_msg,
|
||
"timestamp": now
|
||
})
|
||
|
||
# LOG: Jarvis Antwort hier schreiben!
|
||
log_to_file("J.A.R.V.I.S.", clean_msg)
|
||
|
||
# ============================================
|
||
# COMMANDS AUSFÜHREN
|
||
# ============================================
|
||
|
||
if commands:
|
||
|
||
for target, cmd in commands:
|
||
|
||
target = target.strip()
|
||
cmd = cmd.strip()
|
||
|
||
# ========================================
|
||
# SICHTBARE SYSTEMAKTION
|
||
# ========================================
|
||
|
||
action_msg = (
|
||
f"⚙️ Ich führe jetzt folgenden Befehl "
|
||
f"auf [{target}] aus:\n\n"
|
||
f"{cmd}"
|
||
)
|
||
|
||
await speak_to_user(action_msg)
|
||
|
||
chat_history.append({
|
||
"role": "assistant",
|
||
"content": action_msg,
|
||
"timestamp": now
|
||
})
|
||
|
||
# Optional: Aktion auch ins Log
|
||
log_to_file("SYSTEM", action_msg)
|
||
|
||
# ========================================
|
||
# COMMAND AUSFÜHREN
|
||
# ========================================
|
||
|
||
output = await run_task(
|
||
target,
|
||
cmd
|
||
)
|
||
|
||
# ========================================
|
||
# OUTPUT IM CHAT SICHTBAR MACHEN
|
||
# ========================================
|
||
|
||
if output:
|
||
|
||
output_msg = (
|
||
f"💻 Ergebnis der Ausführung "
|
||
f"auf [{target}]:\n\n"
|
||
f"{output}"
|
||
)
|
||
|
||
else:
|
||
|
||
output_msg = (
|
||
f"✅ Befehl auf [{target}] "
|
||
f"erfolgreich abgeschlossen."
|
||
)
|
||
|
||
await speak_to_user(output_msg)
|
||
|
||
sys_now = datetime.now().strftime(
|
||
"%d.%m.%Y %H:%M"
|
||
)
|
||
|
||
chat_history.append({
|
||
"role": "assistant",
|
||
"content": output_msg,
|
||
"timestamp": sys_now
|
||
})
|
||
|
||
# LOG: System Output hier schreiben!
|
||
log_to_file("SYSTEM", output_msg)
|
||
|
||
# ============================================
|
||
# HISTORY LIMIT
|
||
# ============================================
|
||
|
||
if len(chat_history) > 20:
|
||
chat_history = chat_history[-20:]
|
||
|
||
|
||
# ====================================================
|
||
# START
|
||
# ====================================================
|
||
|
||
if __name__ == "__main__":
|
||
|
||
try:
|
||
|
||
asyncio.run(main_chat_loop())
|
||
|
||
except KeyboardInterrupt:
|
||
|
||
print(
|
||
f"\n{ERROR_COLOR}"
|
||
f"⛔ J.A.R.V.I.S. hart beendet."
|
||
f"{RESET}"
|
||
)
|
||
EOF
|
||
|
||
# J.A.R.V.I.S. Start-Skript
|
||
cat << 'EOF' > "$JARVIS_DIR/start.sh"
|
||
#!/bin/bash
|
||
cd "$(dirname "$0")"
|
||
source venv/bin/activate
|
||
python3 jarvis.py
|
||
EOF
|
||
chmod +x "$JARVIS_DIR/start.sh"
|
||
|
||
# Rechte korrigieren
|
||
chown -R "$REAL_USER:$REAL_USER" "$JARVIS_DIR"
|
||
chown -R "$REAL_USER:$REAL_USER" "$REAL_HOME/.config"
|
||
chown "$REAL_USER:$REAL_USER" "$REAL_HOME/.profile"
|
||
|
||
# Python venv installieren
|
||
echo "🐍 Erstelle Python-Umgebung für J.A.R.V.I.S...."
|
||
sudo -u "$REAL_USER" bash -c "cd $JARVIS_DIR && python3 -m venv venv && ./venv/bin/pip install --upgrade pip && ./venv/bin/pip install -r requirements.txt"
|
||
|
||
# Autostart (Waybar & JARVIS direkt in Tilix öffnen)
|
||
cat << EOF > "$REAL_HOME/.config/labwc/autostart"
|
||
dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=labwc
|
||
waybar &
|
||
tilix --title="J.A.R.V.I.S. Terminal" -e "$JARVIS_DIR/start.sh" &
|
||
EOF
|
||
chmod +x "$REAL_HOME/.config/labwc/autostart"
|
||
chown "$REAL_USER:$REAL_USER" "$REAL_HOME/.config/labwc/autostart"
|
||
|
||
echo "===================================================="
|
||
echo "✅ Lokales Setup erfolgreich abgeschlossen!"
|
||
echo "👉 1. Trage deine API-Keys in $JARVIS_DIR/config/.env ein."
|
||
echo "👉 2. Starte das System neu oder logge dich neu ein."
|
||
echo "===================================================="
|