Compare commits
59 Commits
36eabb27d0
...
cc51044d71
| Author | SHA1 | Date | |
|---|---|---|---|
| cc51044d71 | |||
| 478d6c91ed | |||
| 15e04d54dc | |||
| cdf9f1d34c | |||
| 3293f24965 | |||
| ed7b4c771b | |||
| 55ae56dee1 | |||
| 07bd5df3cd | |||
| 7bcd1f401c | |||
| f46c72ce80 | |||
| e0f2afba77 | |||
| 0fc72d77aa | |||
| 7393f37654 | |||
| 8922d7c918 | |||
| 159782b62f | |||
| a9448dc4b3 | |||
| ab53c68342 | |||
| faa3835f45 | |||
| 4e9b516caa | |||
| fff6f0767f | |||
| 0c21078a79 | |||
| fc71f166e3 | |||
| b9291d9c6b | |||
| 3ed41cf810 | |||
| b5f6ac1701 | |||
| a4b3d58d76 | |||
| 30ca757e34 | |||
| 409c079197 | |||
| 1ad587cdd6 | |||
| f53beea3bd | |||
| e12cf51ffe | |||
| 5748e4c9d3 | |||
| 2d8c35cc44 | |||
| d3b8239f98 | |||
| 7b92eb44ff | |||
| 1b19c368ff | |||
| 1ffb7cec26 | |||
| e5893ec407 | |||
| 2a616632a8 | |||
| d8f8465f1f | |||
| d7c66ca572 | |||
| 259e2f3208 | |||
| 0eefb3b373 | |||
| c40879e75f | |||
| a755d1e6d1 | |||
| 226b1c8a95 | |||
| 44e0ff26fa | |||
| 973fe8ae75 | |||
| 3ed40551ae | |||
| a0d5e4540b | |||
| 90c2439705 | |||
| f17a4fca13 | |||
| a661b357c8 | |||
| 36432cf01a | |||
| a11d68a974 | |||
| b4cafdf462 | |||
| 07fc7d1d7f | |||
| 3828c792e3 | |||
| c18b4e2141 |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
config/.env
|
||||
data/*.db
|
||||
workspace/
|
||||
*/.gitkeep
|
||||
@@ -1,5 +1,9 @@
|
||||
# J.A.R.V.I.S. - AI
|
||||
|
||||
Setup:
|
||||
curl -sSL https://git.pi-farm.de/pi-farm/PiDoBot/raw/branch/main/setup.sh | bash
|
||||
### Setup:
|
||||
|
||||
```
|
||||
curl -sSL https://git.pi-farm.de/pi-farm/PiDoBot/raw/branch/dev/setup.sh -o setup.sh && \
|
||||
chmod +x setup.sh && \
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# --- KI Provider Auswahl (google, openai, oder ollama) ---
|
||||
AI_PROVIDER=google
|
||||
AI_PROVIDER='nvidia'
|
||||
|
||||
# --- API Keys ---
|
||||
GOOGLE_API_KEY=dein-google-key-hier
|
||||
@@ -12,8 +12,10 @@ OPENAI_MODEL=gpt-4o
|
||||
OLLAMA_MODEL=llama3
|
||||
|
||||
# --- Lokale KI (Ollama) ---
|
||||
OLLAMA_BASE_URL=http://127.0.0.1:11434/v1
|
||||
OLLAMA_BASE_URL='http://192.168.178.118:11434/v1'
|
||||
|
||||
TELEGRAM_BOT_TOKEN=dein-telegram-bot-token
|
||||
ALLOWED_TELEGRAM_USER_ID=deine-telegram-id
|
||||
|
||||
WEB_USER_NAME=Tony
|
||||
NVIDIA_MODEL='moonshotai/kimi-k2.5'
|
||||
6
config/group_prompt.txt
Normal file
6
config/group_prompt.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Du bist JARVIS, ein hilfreicher Assistent in dieser Telegram-Gruppe.
|
||||
DEINE ROLLE:
|
||||
- Sei höflich, locker und hilfsbereit.
|
||||
- Du hast in diesem Chat KEINEN Zugriff auf Server-Systeme oder private Dateien.
|
||||
- Wenn dich jemand nach technischen Details zum Cluster fragt, antworte, dass dies nur dem Administrator vorbehalten ist.
|
||||
- Du duzt die Leute und sprichst sie mit ihrem Namen "{user_name}" an.
|
||||
36
config/system_prompt.txt
Normal file
36
config/system_prompt.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
Dein Name ist JARVIS.
|
||||
Du bist ein präziser KI-Assistent für die Cluster-Verwaltung.
|
||||
|
||||
WICHTIGSTE REGEL: Deine Sprache ist locker, technisch versiert und du verwendest NIEMALS die Höflichkeitsform "Sie". Wir sind per Du.
|
||||
|
||||
DEIN WORKSPACE (GEDÄCHTNIS):
|
||||
Du hast Zugriff auf ein eigenes Arbeitsverzeichnis auf dem Host-System (localhost), um dir Notizen zu machen oder Todos für {user_name} zu speichern:
|
||||
- Arbeitsverzeichnis: {workspace_dir}
|
||||
- Notizen-Datei im Mark-Down format: {notes_file}
|
||||
- Todo-Liste im Mark-Down format: {todo_file}
|
||||
|
||||
Du kannst diese Dateien jederzeit lesen oder beschreiben ohne nachzufragen. Nutze dazu normale Shell-Befehle (z.B. cat, echo "text" >> datei, sed) mit dem Ziel "localhost":
|
||||
<EXECUTE target="localhost">befehl</EXECUTE>
|
||||
|
||||
PROTOKOLL FÜR BEFEHLE: Du arbeitest STRENG in zwei Phasen:
|
||||
|
||||
PHASE 1 (Vorschlag):
|
||||
Wenn {user_name} eine Aktion anfordert (z.B. Installation, Update, Ping, oder etwas in deinen Notizen zu speichern), erstelle NUR einen Text-Vorschlag.
|
||||
- Beschreibe kurz, was du tun würdest.
|
||||
- Nenne den Befehl als normalen Text (KEIN XML, KEIN <EXECUTE>).
|
||||
- Frage explizit nach Erlaubnis: "Soll ich das ausführen, {user_name}?"
|
||||
|
||||
PHASE 2 (Ausführung):
|
||||
NUR wenn {user_name} die Aktion bestätigt (z.B. "Ja", "Mach das"), gibst du den Befehl im XML-Format aus:
|
||||
<EXECUTE target="IP_ODER_LOCALHOST">befehl</EXECUTE>
|
||||
|
||||
REGELN, TONFALL UND ANREDE:
|
||||
- Du duzt {user_name} konsequent (nutze "Du", "Dein", niemals "Sie").
|
||||
- Integriere den Namen des Nutzers ({user_name}) natürlich in deine Antwort.
|
||||
- Variiere die Begrüßung: Nutze mal "Hallo {user_name}", mal "Gerne, {user_name}...", oder flechte den Namen mitten im Satz ein.
|
||||
- Vermeide es, den Namen einfach nur wie einen statischen Header ("{user_name}: ...") voranzustellen.
|
||||
- Bekannte Nodes:
|
||||
{node_info}
|
||||
- Nutze für <EXECUTE> bei fernen Rechnern IMMER die IP-Adresse, bei deinen eigenen Dateien IMMER "localhost".
|
||||
- Begrenze endlose Befehle (z.B. ping -c 4).
|
||||
- Führe NIEMALS einen <EXECUTE> Tag in PHASE 1 aus.
|
||||
305
setup.sh
Normal file → Executable file
305
setup.sh
Normal file → Executable file
@@ -1,81 +1,264 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Konfiguration
|
||||
# ==========================================
|
||||
# Farbdefinitionen
|
||||
# ==========================================
|
||||
C_DEF=$'\e[0m' # Default / Reset
|
||||
C_BOLD=$'\e[1m' # Fett
|
||||
C_CYAN=$'\e[1;36m' # Cyan
|
||||
C_BLUE=$'\e[1;34m' # Blau
|
||||
C_GREEN=$'\e[1;32m' # Grün
|
||||
C_YELLOW=$'\e[1;33m' # Gelb
|
||||
C_RED=$'\e[1;31m' # Rot
|
||||
|
||||
REPO_URL="https://git.pi-farm.de/pi-farm/PiDoBot.git"
|
||||
INSTALL_DIR="jarvis-ai"
|
||||
SERVICE_FILE="/etc/systemd/system/jarvis.service"
|
||||
|
||||
echo ">>> Starte Setup für J.A.R.V.I.S. - AI ..."
|
||||
echo -e "${C_CYAN}${C_BOLD}==========================================${C_DEF}"
|
||||
echo -e "${C_CYAN}${C_BOLD}>>> J.A.R.V.I.S.-AI - Setup <<<${C_DEF}"
|
||||
echo -e "${C_CYAN}${C_BOLD}==========================================${C_DEF}"
|
||||
|
||||
# 1. Prüfen, ob Git installiert ist, ansonsten installieren
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "--- Git nicht gefunden. Installiere Git..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git
|
||||
else
|
||||
echo "--- Git ist bereits installiert."
|
||||
fi
|
||||
# 0. Installationsverzeichnis abfragen
|
||||
read -p "${C_CYAN}Installationsverzeichnis (Standard: ${C_YELLOW}/home/pi/jarvis-ai${C_CYAN}): ${C_DEF}" input_dir </dev/tty
|
||||
INSTALL_DIR=${input_dir:-/home/pi/jarvis-ai}
|
||||
|
||||
# 2. Weitere zwingende System-Abhängigkeiten installieren
|
||||
# sshpass: Für das automatische Kopieren des SSH-Keys auf neue Nodes
|
||||
# python3-venv: Für die isolierte Python-Umgebung
|
||||
echo "--- Installiere benötigte System-Pakete..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y wget sshpass python3-pip python3-venv
|
||||
# Pfad normalisieren
|
||||
INSTALL_DIR=$(realpath -m "$INSTALL_DIR")
|
||||
|
||||
# 3. Repository klonen
|
||||
if [ ! -d "$INSTALL_DIR" ]; then
|
||||
echo "--- Klone Repository von $REPO_URL..."
|
||||
git clone --branch main --single-branch "$REPO_URL" "$INSTALL_DIR"
|
||||
else
|
||||
echo "--- Verzeichnis $INSTALL_DIR existiert bereits. Überspringe Klonen..."
|
||||
fi
|
||||
|
||||
# In das Verzeichnis wechseln
|
||||
echo -e "\n${C_GREEN}Zielverzeichnis: ${C_BOLD}$INSTALL_DIR${C_DEF}"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR" || exit
|
||||
|
||||
# 4. Virtual Environment und Python-Abhängigkeiten einrichten
|
||||
echo "--- Erstelle Python Virtual Environment..."
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
# 1. System-Abhängigkeiten
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 1. Prüfe und installiere System-Pakete...${C_DEF}"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git wget sshpass python3-pip python3-venv iproute2
|
||||
|
||||
echo "--- Installiere Python-Pakete aus requirements.txt..."
|
||||
pip install --upgrade pip
|
||||
if [ -f "requirements.txt" ]; then
|
||||
pip install -r requirements.txt
|
||||
# 2. Repository klonen
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 2. Hole Quellcode...${C_DEF}"
|
||||
if [ ! -d ".git" ]; then
|
||||
git clone --branch dev --single-branch "$REPO_URL" .
|
||||
else
|
||||
echo "FEHLER: requirements.txt nicht im Repository gefunden!"
|
||||
echo -e "${C_YELLOW}Repo bereits vorhanden, aktualisiere...${C_DEF}"
|
||||
git pull
|
||||
fi
|
||||
|
||||
# 3. Virtual Environment
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 3. Richte Python-Umgebung ein...${C_DEF}"
|
||||
if [ ! -d "venv" ]; then
|
||||
python3 -m venv venv
|
||||
fi
|
||||
# Nutze den direkten Pfad zum Pip im Venv
|
||||
./venv/bin/pip install --upgrade pip
|
||||
if [ -f "source/requirements.txt" ]; then
|
||||
./venv/bin/pip install -r source/requirements.txt
|
||||
else
|
||||
echo -e "${C_RED}❌ FEHLER: requirements.txt nicht gefunden!${C_DEF}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. SSH-Key für den Master prüfen/erstellen (für passwortlosen Zugriff auf Nodes)
|
||||
# 4. SSH-Key & Konfiguration (Silent Mode)
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 4. Prüfe SSH-Schlüssel und Konfiguration...${C_DEF}"
|
||||
if [ ! -f "$HOME/.ssh/id_rsa" ]; then
|
||||
echo "--- Generiere SSH-Key für die passwortlose Kommunikation..."
|
||||
ssh-keygen -t rsa -N "" -f "$HOME/.ssh/id_rsa"
|
||||
echo -e "${C_GREEN}✅ SSH-Key generiert.${C_DEF}"
|
||||
else
|
||||
echo "--- SSH-Key existiert bereits."
|
||||
echo -e "${C_GREEN}✅ SSH-Key existiert bereits.${C_DEF}"
|
||||
fi
|
||||
|
||||
# 6. Static Ordner anlegen und Dateien herunterladen
|
||||
if [ ! -f "./static" ]; then
|
||||
echo "--- Static-Ordner anlegen und Dateien herunterladen..."
|
||||
mkdir -p static
|
||||
cd static
|
||||
# GridStack
|
||||
wget https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack.min.css
|
||||
wget https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all.js
|
||||
# Xterm
|
||||
wget https://cdn.jsdelivr.net/npm/xterm@5.1.0/css/xterm.css
|
||||
wget https://cdn.jsdelivr.net/npm/xterm@5.1.0/lib/xterm.js
|
||||
# Xterm Fit Addon
|
||||
wget https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.js
|
||||
# marked.js
|
||||
wget https://cdn.jsdelivr.net/npm/marked/marked.min.js
|
||||
cd ..
|
||||
else
|
||||
echo "--- Static-Dateien existieren bereits."
|
||||
fi
|
||||
echo ">>> Installation abgeschlossen!"
|
||||
echo "--- Starte J.A.R.V.I.S. - AI auf Port 8000..."
|
||||
# SSH 'Silent Mode' einrichten, um Spam im Log zu verhindern
|
||||
mkdir -p "$HOME/.ssh"
|
||||
if [ ! -f "$HOME/.ssh/config" ] || ! grep -q "StrictHostKeyChecking no" "$HOME/.ssh/config"; then
|
||||
echo -e "${C_YELLOW}Richte SSH Silent-Mode für lokale Netze ein...${C_DEF}"
|
||||
cat <<EOF >> "$HOME/.ssh/config"
|
||||
|
||||
# 6. Programm starten
|
||||
python3 -m uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
Host 192.168.* 10.* 172.*
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
LogLevel ERROR
|
||||
EOF
|
||||
chmod 600 "$HOME/.ssh/config"
|
||||
fi
|
||||
|
||||
# 5. Static Dateien
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 5. Lade Frontend-Bibliotheken...${C_DEF}"
|
||||
mkdir -p source/static
|
||||
cd source/static
|
||||
wget -nc -q https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack.min.css
|
||||
wget -nc -q https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all.js
|
||||
wget -nc -q https://cdn.jsdelivr.net/npm/xterm@5.1.0/css/xterm.css
|
||||
wget -nc -q https://cdn.jsdelivr.net/npm/xterm@5.1.0/lib/xterm.js
|
||||
wget -nc -q https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.js
|
||||
wget -nc -q https://cdn.jsdelivr.net/npm/marked/marked.min.js
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# 6. .env Setup
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 6. Konfiguration (.env)...${C_DEF}"
|
||||
mkdir -p config
|
||||
ENV_FILE="config/.env"
|
||||
|
||||
# Standardwerte definieren
|
||||
web_user="Tony"
|
||||
ai_prov="google"
|
||||
google_key=""
|
||||
openai_key=""
|
||||
nvidia_key=""
|
||||
ollama_url="http://127.0.0.1:11434/v1"
|
||||
google_mod="gemini-2.5-flash"
|
||||
openai_mod="gpt-4o"
|
||||
tg_token=""
|
||||
tg_id=""
|
||||
|
||||
# Falls die Datei existiert, alte Werte laden
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo -e "${C_YELLOW}Bestehende .env gefunden. Lade aktuelle Werte...${C_DEF}"
|
||||
set -a
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
web_user="${WEB_USER_NAME:-$web_user}"
|
||||
ai_prov="${AI_PROVIDER:-$ai_prov}"
|
||||
google_key="${GOOGLE_API_KEY:-$google_key}"
|
||||
openai_key="${OPENAI_API_KEY:-$openai_key}"
|
||||
nvidia_key="${NVIDIA_API_KEY:-$nvidia_key}"
|
||||
ollama_url="${OLLAMA_BASE_URL:-$ollama_url}"
|
||||
google_mod="${GOOGLE_MODEL:-$google_mod}"
|
||||
openai_mod="${OPENAI_MODEL:-$openai_mod}"
|
||||
tg_token="${TELEGRAM_BOT_TOKEN:-$tg_token}"
|
||||
tg_id="${ALLOWED_TELEGRAM_USER_ID:-$tg_id}"
|
||||
fi
|
||||
|
||||
echo -e "\nBitte gib die neuen Werte ein. (Drücke ENTER, um den Wert in Klammern beizubehalten):"
|
||||
|
||||
# Farbige Prompts
|
||||
read -p "${C_CYAN}Dein Web-Benutzername [${C_YELLOW}$web_user${C_CYAN}]: ${C_DEF}" input_web_user </dev/tty
|
||||
web_user=${input_web_user:-$web_user}
|
||||
|
||||
read -p "${C_CYAN}Primäre KI (google, openai, nvidia, ollama) [${C_YELLOW}$ai_prov${C_CYAN}]: ${C_DEF}" input_ai_prov </dev/tty
|
||||
ai_prov=${input_ai_prov:-$ai_prov}
|
||||
|
||||
disp_gkey=$( [ -n "$google_key" ] && echo "${google_key:0:5}***" || echo "" )
|
||||
read -p "${C_CYAN}Google Gemini API Key [${C_YELLOW}$disp_gkey${C_CYAN}]: ${C_DEF}" input_google_key </dev/tty
|
||||
google_key=${input_google_key:-$google_key}
|
||||
|
||||
disp_okey=$( [ -n "$openai_key" ] && echo "${openai_key:0:5}***" || echo "" )
|
||||
read -p "${C_CYAN}OpenAI API Key [${C_YELLOW}$disp_okey${C_CYAN}]: ${C_DEF}" input_openai_key </dev/tty
|
||||
openai_key=${input_openai_key:-$openai_key}
|
||||
|
||||
disp_nkey=$( [ -n "$nvidia_key" ] && echo "${nvidia_key:0:5}***" || echo "" )
|
||||
read -p "${C_CYAN}NVIDIA API Key [${C_YELLOW}$disp_nkey${C_CYAN}]: ${C_DEF}" input_nvidia_key </dev/tty
|
||||
nvidia_key=${input_nvidia_key:-$nvidia_key}
|
||||
|
||||
read -p "${C_CYAN}Ollama Base URL [${C_YELLOW}$ollama_url${C_CYAN}]: ${C_DEF}" input_ollama_url </dev/tty
|
||||
ollama_url=${input_ollama_url:-$ollama_url}
|
||||
|
||||
disp_tgtoken=$( [ -n "$tg_token" ] && echo "${tg_token:0:8}***" || echo "" )
|
||||
read -p "${C_CYAN}Telegram Bot Token [${C_YELLOW}$disp_tgtoken${C_CYAN}]: ${C_DEF}" input_tg_token </dev/tty
|
||||
tg_token=${input_tg_token:-$tg_token}
|
||||
|
||||
read -p "${C_CYAN}Erlaubte Telegram User ID [${C_YELLOW}$tg_id${C_CYAN}]: ${C_DEF}" input_tg_id </dev/tty
|
||||
tg_id=${input_tg_id:-$tg_id}
|
||||
|
||||
# Neue .env schreiben
|
||||
cat <<EOF > "$ENV_FILE"
|
||||
WEB_USER_NAME=$web_user
|
||||
AI_PROVIDER=$ai_prov
|
||||
GOOGLE_API_KEY=$google_key
|
||||
OPENAI_API_KEY=$openai_key
|
||||
NVIDIA_API_KEY=$nvidia_key
|
||||
OLLAMA_BASE_URL=$ollama_url
|
||||
GOOGLE_MODEL=$google_mod
|
||||
OPENAI_MODEL=$openai_mod
|
||||
TELEGRAM_BOT_TOKEN=$tg_token
|
||||
ALLOWED_TELEGRAM_USER_ID=$tg_id
|
||||
EOF
|
||||
echo -e "${C_GREEN}✅ Konfiguration erfolgreich gespeichert.${C_DEF}"
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Weiche: Update vs. Neuinstallation
|
||||
# ==========================================
|
||||
|
||||
if [ -f "$SERVICE_FILE" ]; then
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 7. Update-Modus: Bestehender Dienst gefunden...${C_DEF}"
|
||||
echo -e "${C_YELLOW}Überspringe Port- und Firewall-Check, starte Dienst neu...${C_DEF}"
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart jarvis.service
|
||||
|
||||
echo -e "\n${C_GREEN}${C_BOLD}==========================================${C_DEF}"
|
||||
echo -e "${C_GREEN}✅ Update komplett! J.A.R.V.I.S. wurde erfolgreich aktualisiert und neu gestartet.${C_DEF}"
|
||||
echo -e "Verzeichnis: ${C_BOLD}$INSTALL_DIR${C_DEF}"
|
||||
echo -e "Web-Interface: ${C_CYAN}http://<deine-ip>:8000${C_DEF}"
|
||||
echo -e "Log-Ausgabe: ${C_YELLOW}sudo journalctl -u jarvis -f${C_DEF}"
|
||||
echo -e "${C_GREEN}${C_BOLD}==========================================${C_DEF}"
|
||||
else
|
||||
|
||||
# 7. Port Check
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 7. Prüfe Port 8000...${C_DEF}"
|
||||
if ss -tuln | grep -q ":8000 "; then
|
||||
echo -e "${C_RED}⚠️ WARNUNG: Port 8000 wird bereits von einem anderen Prozess verwendet!${C_DEF}"
|
||||
echo "J.A.R.V.I.S. wird möglicherweise nicht starten können. Bitte prüfe dies nach dem Setup."
|
||||
read -p "Drücke ENTER, um trotzdem fortzufahren..." </dev/tty
|
||||
else
|
||||
echo -e "${C_GREEN}✅ Port 8000 ist frei.${C_DEF}"
|
||||
fi
|
||||
|
||||
# 8. Firewall Check
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 8. Prüfe Firewall-Einstellungen...${C_DEF}"
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
if sudo ufw status | grep -qw "active"; then
|
||||
echo -e "${C_YELLOW}UFW Firewall ist aktiv. Öffne Port 8000...${C_DEF}"
|
||||
sudo ufw allow 8000/tcp
|
||||
echo -e "${C_GREEN}✅ Port 8000 (TCP) in UFW freigegeben.${C_DEF}"
|
||||
else
|
||||
echo -e "${C_GREEN}✅ UFW ist installiert, aber inaktiv. Keine Blockade zu erwarten.${C_DEF}"
|
||||
fi
|
||||
else
|
||||
echo -e "${C_GREEN}✅ Keine UFW Firewall gefunden. Überspringe Firewall-Setup.${C_DEF}"
|
||||
fi
|
||||
|
||||
# 9. Systemd Service einrichten
|
||||
echo -e "\n${C_BLUE}${C_BOLD}--- 9. Systemdienst (Autostart) einrichten...${C_DEF}"
|
||||
read -p "${C_CYAN}Möchtest du J.A.R.V.I.S. als Hintergrunddienst installieren? (j/N): ${C_DEF}" setup_service </dev/tty
|
||||
|
||||
if [[ "$setup_service" =~ ^[jJ]$ ]]; then
|
||||
echo "Erstelle Service-Datei in $SERVICE_FILE..."
|
||||
sudo bash -c "cat <<EOF > $SERVICE_FILE
|
||||
[Unit]
|
||||
Description=J.A.R.V.I.S. AI Web und Telegram Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=$USER
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
Environment=\"PYTHONPATH=$INSTALL_DIR/source\"
|
||||
ExecStart=$INSTALL_DIR/venv/bin/python -m uvicorn source.main:app --host 0.0.0.0 --port 8000 --proxy-headers
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF"
|
||||
|
||||
echo "Aktiviere und starte den Dienst..."
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable jarvis.service
|
||||
sudo systemctl restart jarvis.service
|
||||
|
||||
echo -e "\n${C_GREEN}${C_BOLD}==========================================${C_DEF}"
|
||||
echo -e "${C_GREEN}✅ Setup komplett! J.A.R.V.I.S. läuft als Dienst.${C_DEF}"
|
||||
echo -e "Verzeichnis: ${C_BOLD}$INSTALL_DIR${C_DEF}"
|
||||
echo -e "Web-Interface: ${C_CYAN}http://<deine-ip>:8000${C_DEF}"
|
||||
echo -e "Log-Ausgabe: ${C_YELLOW}sudo journalctl -u jarvis -f${C_DEF}"
|
||||
echo -e "${C_GREEN}${C_BOLD}==========================================${C_DEF}"
|
||||
else
|
||||
echo -e "\n${C_GREEN}${C_BOLD}==========================================${C_DEF}"
|
||||
echo -e "${C_GREEN}✅ Setup komplett! Du kannst J.A.R.V.I.S. nun manuell starten:${C_DEF}"
|
||||
echo "cd $INSTALL_DIR"
|
||||
echo "source venv/bin/activate"
|
||||
echo "export PYTHONPATH=\$PYTHONPATH:\$(pwd)/source"
|
||||
echo "python3 -m uvicorn source.main:app --host 0.0.0.0 --port 8000"
|
||||
echo -e "${C_GREEN}${C_BOLD}==========================================${C_DEF}"
|
||||
fi
|
||||
fi
|
||||
@@ -9,6 +9,8 @@ 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
|
||||
@@ -20,20 +22,53 @@ from fastapi.responses import RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from dotenv import load_dotenv, set_key
|
||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
# Lade Umgebungsvariablen
|
||||
load_dotenv()
|
||||
# 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")
|
||||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
#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")
|
||||
DB_PATH = "cluster.db"
|
||||
chat_history = []
|
||||
PROMPT_FILE = "system_prompt.txt"
|
||||
ENV_FILE = os.path.join(os.path.dirname(__file__), ".env")
|
||||
# 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()
|
||||
@@ -79,7 +114,25 @@ def get_db():
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def get_system_prompt():
|
||||
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()
|
||||
@@ -89,27 +142,24 @@ def get_system_prompt():
|
||||
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"
|
||||
|
||||
if os.path.exists(PROMPT_FILE):
|
||||
with open(PROMPT_FILE, "r", encoding="utf-8") as f:
|
||||
template = f.read()
|
||||
else:
|
||||
template = "Du bist ein Cluster-Orchestrator. Nodes:\n{node_info}\nBefehle via <EXECUTE target=\"IP\">cmd</EXECUTE>"
|
||||
print(f"⚠️ Warnung: {PROMPT_FILE} fehlt.")
|
||||
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 template.replace("{node_info}", node_info)
|
||||
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:]
|
||||
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"]:
|
||||
messages = [{"role": "system", "content": system_prompt}] + chat_history
|
||||
# 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
|
||||
@@ -126,7 +176,6 @@ async def get_ai_response(user_input, system_prompt):
|
||||
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,
|
||||
@@ -135,40 +184,36 @@ async def get_ai_response(user_input, system_prompt):
|
||||
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]:
|
||||
# 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 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)
|
||||
# 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}")
|
||||
|
||||
# 3. Die Antwort der KI ebenfalls ins Gedächtnis aufnehmen
|
||||
chat_history.append({"role": "assistant", "content": ai_msg})
|
||||
|
||||
# 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):
|
||||
@@ -178,30 +223,62 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_
|
||||
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. Gruppen-Logik: Nur reagieren, wenn der Bot @erwähnt wird
|
||||
# 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, damit die KI nicht verwirrt wird
|
||||
# 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:
|
||||
# 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:
|
||||
# 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
|
||||
ai_response = await get_ai_response(user_msg, get_system_prompt())
|
||||
# 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'<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-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)
|
||||
@@ -223,7 +300,7 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_
|
||||
if n:
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
f"ssh -o StrictHostKeyChecking=no {n['user']}@{n['ip']} '{cmd}'",
|
||||
f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR {n['user']}@{n['ip']} '{cmd}'",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT
|
||||
)
|
||||
@@ -233,12 +310,18 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_
|
||||
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}"})
|
||||
# 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():
|
||||
@@ -290,7 +373,7 @@ 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}\""
|
||||
ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -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')
|
||||
@@ -306,7 +389,7 @@ async def get_remote_info(ip, user):
|
||||
# 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}\""
|
||||
ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{win_cmd}\""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(ssh_cmd, shell=True).decode().strip().split('\n')
|
||||
@@ -341,7 +424,7 @@ async def bootstrap_node(ip, user, password):
|
||||
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}\""
|
||||
setup_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null {user}@{ip} \"{cmd_universal}\""
|
||||
|
||||
try:
|
||||
# Wir führen es aus. Das "2x Passwort"-Problem kommt oft von TTY-Anfragen.
|
||||
@@ -554,36 +637,80 @@ async def terminal_websocket(websocket: WebSocket, ip: str):
|
||||
@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()
|
||||
ai_response = await get_ai_response(user_msg, get_system_prompt())
|
||||
|
||||
# 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'<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)
|
||||
|
||||
# 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: tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip()))
|
||||
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)
|
||||
summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt())
|
||||
# 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: pass
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Webchat Fehler: {e}")
|
||||
|
||||
async def run_remote_task(ip, user, cmd):
|
||||
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)
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -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"
|
||||
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'}"})
|
||||
|
||||
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")
|
||||
@@ -693,6 +820,19 @@ async def get_models(provider: str, url: str = None):
|
||||
print(f"Fehler beim Abrufen der Modelle für {provider}: {str(e)}")
|
||||
return {"models": []} # Gibt eine leere Liste zurück -> Frontend nutzt Fallback
|
||||
|
||||
app.add_middleware(
|
||||
TrustedHostMiddleware,
|
||||
allowed_hosts=["jarvis.pi-farm.de", "192.168.178.13", "localhost"]
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/debug_keys")
|
||||
async def debug_keys():
|
||||
return {
|
||||
0
source/static/.gitkeep
Normal file
0
source/static/.gitkeep
Normal file
583
source/templates/index.html
Normal file
583
source/templates/index.html
Normal file
@@ -0,0 +1,583 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>J.A.R.V.I.S. AI Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<link href="/static/gridstack.min.css" rel="stylesheet"/>
|
||||
<script src="/static/gridstack-all.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/static/xterm.css" />
|
||||
<script src="/static/xterm.js"></script>
|
||||
<script src="/static/xterm-addon-fit.js"></script>
|
||||
<script src="/static/marked.min.js"></script>
|
||||
|
||||
<style>
|
||||
body { margin: 0; background: #0f172a; }
|
||||
|
||||
.grid-stack {
|
||||
background: transparent;
|
||||
min-height: calc(100vh - 60px);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.grid-stack-item-content {
|
||||
background: #1e293b;
|
||||
color: white;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
background: black;
|
||||
}
|
||||
|
||||
#install-log {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
color: #4ade80;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
background: #334155;
|
||||
padding: 8px 12px;
|
||||
cursor: move;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.top-toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: #1e293b;
|
||||
color: white;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.toolbar-title { font-weight: bold; font-size: 1.1em; color: #38bdf8; }
|
||||
.toolbar-controls { display: flex; align-items: center; gap: 10px; }
|
||||
.toolbar-controls label { white-space: nowrap; font-size: 12px; color: #94a3b8; }
|
||||
|
||||
#ollama-url-container {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-left: 1px solid #475569;
|
||||
padding-left: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#ollama-url { width: 100%; min-width: 200px; }
|
||||
.toolbar-controls select, .toolbar-controls input {
|
||||
height: 32px; background: #0f172a; color: white;
|
||||
border: 1px solid #475569; border-radius: 4px; padding: 0 8px; font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-tool {
|
||||
height: 32px; padding: 0 12px; border-radius: 4px; font-size: 12px;
|
||||
font-weight: bold; display: flex; align-items: center; gap: 5px;
|
||||
}
|
||||
.save-btn { background-color: #059669; color: white; }
|
||||
.add-node-btn { background-color: #2563eb; color: white; }
|
||||
|
||||
.node-badge {
|
||||
background: #334155; padding: 2px 5px; border-radius: 4px;
|
||||
font-size: 8px; text-transform: uppercase; font-weight: bold; color: #94a3b8;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.7em;
|
||||
color: #888; /* Ein dezentes Grau */
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Optional: Falls der KI-Text dunkel und der User-Text auf farbigem Grund ist */
|
||||
.message.user .timestamp {
|
||||
color: #e0e0e0; /* Helleres Grau, falls deine User-Bubble z.B. blau ist */
|
||||
}
|
||||
|
||||
.markdown-content pre { background: #000; padding: 8px; border-radius: 4px; border: 1px solid #334155; overflow-x: auto; margin: 8px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-white">
|
||||
|
||||
<div class="top-toolbar">
|
||||
<div class="toolbar-title">🤖 J.A.R.V.I.S. AI Dashboard</div>
|
||||
<div class="toolbar-controls">
|
||||
<button onclick="addNode()" class="btn-tool add-node-btn"><span>+</span> Node</button>
|
||||
<div class="h-6 w-px bg-slate-700 mx-1"></div>
|
||||
<label>Provider:</label>
|
||||
<select id="ai-provider" onchange="updateModelDropdown(false)">
|
||||
<option value="google">Google Gemini</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="nvidia">NVIDIA</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
</select>
|
||||
<label>Modell:</label>
|
||||
<select id="ai-model"></select>
|
||||
<div id="ollama-url-container">
|
||||
<input type="text" id="ollama-url" onblur="updateModelDropdown(false)" placeholder="URL: http://...">
|
||||
</div>
|
||||
<button class="btn-tool save-btn" onclick="saveSettings()">Speichern</button>
|
||||
<span id="settings-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex min-h-screen pt-[55px]">
|
||||
|
||||
<div class="w-64 bg-slate-900 border-r border-slate-800 p-4 flex flex-col fixed left-0 top-[55px] bottom-0 z-10">
|
||||
<div id="node-list" class="flex-1 overflow-y-auto space-y-2">
|
||||
{% for node in nodes %}
|
||||
<div class="p-3 bg-slate-800 rounded border border-slate-700 relative group" id="node-card-{{ node.id }}">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="text-sm font-bold" id="node-name-{{ node.id }}">{{ node.name }}</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="editNode({{ node.id }}, '{{ node.name }}', '{{ node.ip }}')" class="text-slate-500 hover:text-sky-400 text-xs">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
||||
</button>
|
||||
<form action="/remove_node/{{ node.id }}" method="post" onsubmit="return confirm('Entfernen?')">
|
||||
<button type="submit" class="text-slate-500 hover:text-red-500 text-xs">✕</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-500 font-mono flex justify-between items-center mb-1">
|
||||
<span id="node-ip-{{ node.id }}">{{ node.ip }}</span>
|
||||
<button onclick="refreshNodeStatus({{ node.id }})" class="hover:text-blue-400">🔄</button>
|
||||
</div>
|
||||
<div class="flex gap-1 mb-2">
|
||||
<span id="os-{{ node.id }}" class="node-badge truncate max-w-[80px]">{{ node.os }}</span>
|
||||
<span id="arch-{{ node.id }}" class="node-badge">{{ node.arch }}</span>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<span id="led-{{ node.id }}" class="h-2 w-2 rounded-full {% if 'Aktiv' in node.status %} bg-blue-500 shadow-[0_0_8px_#3b82f6] {% else %} bg-yellow-500 {% endif %}"></span>
|
||||
<span id="badge-{{ node.id }}" class="text-[9px] uppercase font-mono text-slate-300">{{ node.status }}</span>
|
||||
</div>
|
||||
<button onclick="openTerminal('{{ node.ip }}')" class="mt-2 w-full text-[10px] bg-slate-700 hover:bg-slate-600 py-1 rounded">Konsole</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button onclick="localStorage.removeItem('pi-orch-layout-v2'); location.reload();" class="mt-4 text-[10px] text-slate-500 hover:text-white uppercase">Layout Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 ml-64 bg-slate-950 overflow-y-auto">
|
||||
<div class="grid-stack">
|
||||
<div class="grid-stack-item" gs-id="chat-widget" gs-w="6" gs-h="5" gs-x="0" gs-y="0">
|
||||
<div class="grid-stack-item-content">
|
||||
<div class="widget-header"><span>💬 J.A.R.V.I.S. - Chat</span> <span>⠿</span></div>
|
||||
<div id="chat-window" class="flex-1 p-4 overflow-y-auto text-sm space-y-2 bg-[#0f172a]/50"></div>
|
||||
<div class="p-2 border-t border-slate-700 flex bg-slate-800">
|
||||
<textarea id="user-input"
|
||||
class="flex-1 bg-slate-900 p-2 rounded text-xs outline-none border border-slate-700 focus:border-blue-500 resize-none overflow-y-hidden"
|
||||
placeholder="Frage eingeben... (Shift+Enter für neue Zeile)"
|
||||
rows="1"
|
||||
oninput="this.style.height = ''; this.style.height = this.scrollHeight + 'px'"></textarea>
|
||||
<button onclick="sendMessage()" class="ml-2 bg-blue-600 hover:bg-blue-500 px-4 py-1 rounded text-xs">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-stack-item" gs-id="logs-widget" gs-w="6" gs-h="5" gs-x="6" gs-y="0">
|
||||
<div class="grid-stack-item-content">
|
||||
<div class="widget-header"><span>📜 System Logs</span> <span>⠿</span></div>
|
||||
<div id="install-log">Warte auf Daten...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-stack-item" gs-id="term-widget" gs-w="12" gs-h="6" gs-x="0" gs-y="5">
|
||||
<div class="grid-stack-item-content">
|
||||
<div class="widget-header"><span>🖥️ Live Terminal</span> <span>⠿</span></div>
|
||||
<div id="terminal" class="terminal-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-node-modal" class="hidden fixed inset-0 bg-black/80 flex items-center justify-center z-[2000]">
|
||||
<div class="bg-slate-800 p-6 rounded-lg border border-slate-600 w-96">
|
||||
<h3 class="text-lg font-bold mb-4">Neuen Node hinzufügen</h3>
|
||||
<form action="/add_node" method="post" class="space-y-4">
|
||||
<input type="text" name="name" placeholder="Name" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm" required>
|
||||
<input type="text" name="ip" placeholder="IP Adresse" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm" required>
|
||||
<input type="text" name="user" placeholder="SSH Benutzer" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm" required>
|
||||
<input type="password" name="password" placeholder="Passwort" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm" required>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button type="button" onclick="closeAddNode()" class="px-4 py-2 text-sm text-slate-400">Abbrechen</button>
|
||||
<button type="submit" class="bg-blue-600 px-4 py-2 text-sm rounded font-bold">Koppeln</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="edit-node-modal" class="hidden fixed inset-0 bg-black/80 flex items-center justify-center z-[2000] p-4">
|
||||
<div class="bg-slate-800 p-6 rounded-lg border border-slate-600 w-full max-w-xl">
|
||||
<h3 class="text-lg font-bold mb-4 text-sky-400">Node-Konfiguration bearbeiten</h3>
|
||||
<input type="hidden" id="edit-node-id">
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-[10px] uppercase text-slate-400 font-bold">Anzeigename</label>
|
||||
<input type="text" id="edit-node-name" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] uppercase text-slate-400 font-bold">SSH Benutzer</label>
|
||||
<input type="text" id="edit-node-user" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] uppercase text-slate-400 font-bold">Betriebssystem</label>
|
||||
<input type="text" id="edit-node-os" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] uppercase text-slate-400 font-bold">Status-Text</label>
|
||||
<input type="text" id="edit-node-status" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-[10px] uppercase text-slate-400 font-bold">IP Adresse</label>
|
||||
<input type="text" id="edit-node-ip" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] uppercase text-slate-400 font-bold">Sudo Passwort</label>
|
||||
<input type="password" id="edit-node-password" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] uppercase text-slate-400 font-bold">Architektur</label>
|
||||
<input type="text" id="edit-node-arch" class="w-full bg-slate-900 border border-slate-700 p-2 rounded text-sm text-white focus:border-sky-500 outline-none">
|
||||
</div>
|
||||
<div class="flex items-center h-full pt-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<input type="checkbox" id="edit-node-docker" class="w-4 h-4 rounded border-slate-700 bg-slate-900 text-sky-600 focus:ring-sky-500">
|
||||
<span class="text-sm text-slate-300 group-hover:text-white transition-colors">Docker installiert</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6 pt-4 border-t border-slate-700">
|
||||
<button type="button" onclick="closeEditNode()" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">Abbrechen</button>
|
||||
<button type="button" onclick="updateNode()" class="bg-sky-600 hover:bg-sky-500 px-6 py-2 text-sm rounded font-bold transition-all shadow-lg shadow-sky-900/20">Änderungen speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentSettings = {};
|
||||
let termDataDisposable = null;
|
||||
let term, fitAddon;
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHost = window.location.host;
|
||||
|
||||
// 1. CHAT LOGIK
|
||||
function initChat(chatWs) {
|
||||
const input = document.getElementById('user-input');
|
||||
|
||||
window.sendMessage = function() {
|
||||
const msg = input.value.trim();
|
||||
if(!msg) return;
|
||||
chatWs.send(msg);
|
||||
appendChat("Du", msg, "text-slate-400 font-bold");
|
||||
|
||||
// Input zurücksetzen
|
||||
input.value = '';
|
||||
input.style.height = 'auto'; // Höhe wieder einklappen
|
||||
input.focus();
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
// Wenn ENTER gedrückt wird OHNE Shift -> Senden
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault(); // Verhindert den Zeilenumbruch
|
||||
sendMessage();
|
||||
}
|
||||
// Wenn ENTER + Shift gedrückt wird -> Standardverhalten (neue Zeile) bleibt
|
||||
});
|
||||
}
|
||||
|
||||
// 2. MODAL LOGIK
|
||||
window.addNode = () => document.getElementById('add-node-modal').classList.remove('hidden');
|
||||
window.closeAddNode = () => document.getElementById('add-node-modal').classList.add('hidden');
|
||||
|
||||
// EDIT LOGIK
|
||||
// Ruft alle Daten ab, bevor das Modal öffnet
|
||||
window.editNode = async (id) => {
|
||||
try {
|
||||
const res = await fetch(`/api/node/${id}`);
|
||||
const node = await res.json();
|
||||
|
||||
document.getElementById('edit-node-id').value = node.id;
|
||||
document.getElementById('edit-node-name').value = node.name;
|
||||
document.getElementById('edit-node-ip').value = node.ip;
|
||||
document.getElementById('edit-node-user').value = node.user;
|
||||
document.getElementById('edit-node-password').value = node.sudo_password;
|
||||
document.getElementById('edit-node-os').value = node.os;
|
||||
document.getElementById('edit-node-arch').value = node.arch;
|
||||
document.getElementById('edit-node-status').value = node.status;
|
||||
document.getElementById('edit-node-docker').checked = node.docker_installed === 1;
|
||||
|
||||
document.getElementById('edit-node-modal').classList.remove('hidden');
|
||||
} catch (e) { alert("Fehler beim Laden der Node-Daten"); }
|
||||
};
|
||||
window.closeEditNode = () => document.getElementById('edit-node-modal').classList.add('hidden');
|
||||
|
||||
window.updateNode = async function() {
|
||||
const id = document.getElementById('edit-node-id').value;
|
||||
const payload = {
|
||||
name: document.getElementById('edit-node-name').value,
|
||||
ip: document.getElementById('edit-node-ip').value,
|
||||
user: document.getElementById('edit-node-user').value,
|
||||
sudo_password: document.getElementById('edit-node-password').value,
|
||||
os: document.getElementById('edit-node-os').value,
|
||||
arch: document.getElementById('edit-node-arch').value,
|
||||
status: document.getElementById('edit-node-status').value,
|
||||
docker_installed: document.getElementById('edit-node-docker').checked ? 1 : 0
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/node/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if(res.ok) {
|
||||
location.reload(); // Einfachste Methode, um alle Badges zu aktualisieren
|
||||
}
|
||||
} catch (e) { alert("Fehler beim Speichern"); }
|
||||
};
|
||||
|
||||
// 3. ERWEITERTER REFRESH
|
||||
window.refreshNodeStatus = async function(nodeId) {
|
||||
const badge = document.getElementById(`badge-${nodeId}`);
|
||||
const led = document.getElementById(`led-${nodeId}`);
|
||||
const osEl = document.getElementById(`os-${nodeId}`);
|
||||
const archEl = document.getElementById(`arch-${nodeId}`);
|
||||
const card = document.getElementById(`node-card-${nodeId}`);
|
||||
|
||||
badge.textContent = "PRÜFE...";
|
||||
card.style.opacity = "0.7";
|
||||
|
||||
try {
|
||||
const response = await fetch(`/refresh_status/${nodeId}`);
|
||||
const data = await response.json();
|
||||
|
||||
badge.textContent = data.status;
|
||||
osEl.textContent = data.os;
|
||||
archEl.textContent = data.arch;
|
||||
|
||||
if (data.status.includes("Aktiv")) {
|
||||
led.className = "h-2 w-2 rounded-full bg-blue-500 shadow-[0_0_8px_#3b82f6]";
|
||||
} else if (data.status === "Offline") {
|
||||
led.className = "h-2 w-2 rounded-full bg-red-500 shadow-[0_0_8px_#ef4444]";
|
||||
} else {
|
||||
led.className = "h-2 w-2 rounded-full bg-yellow-500";
|
||||
}
|
||||
} catch (e) {
|
||||
badge.textContent = "FEHLER";
|
||||
led.className = "h-2 w-2 rounded-full bg-red-500";
|
||||
} finally {
|
||||
card.style.opacity = "1";
|
||||
}
|
||||
};
|
||||
|
||||
// 4. SETTINGS & MODEL LOGIK
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const res = await fetch('/api/settings');
|
||||
currentSettings = await res.json();
|
||||
document.getElementById('ai-provider').value = currentSettings.provider;
|
||||
document.getElementById('ollama-url').value = currentSettings.ollama_base_url || "http://127.0.0.1:11434/v1";
|
||||
updateModelDropdown(true);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function updateModelDropdown(isInitialLoad = false) {
|
||||
const provider = document.getElementById('ai-provider').value;
|
||||
const modelSelect = document.getElementById('ai-model');
|
||||
const urlContainer = document.getElementById('ollama-url-container');
|
||||
const ollamaUrl = document.getElementById('ollama-url').value;
|
||||
|
||||
urlContainer.style.display = (provider === "ollama") ? "flex" : "none";
|
||||
modelSelect.innerHTML = '<option>Lade...</option>';
|
||||
|
||||
try {
|
||||
let queryUrl = `/api/models?provider=${provider}`;
|
||||
if (provider === "ollama") {
|
||||
queryUrl += `&url=${encodeURIComponent(ollamaUrl)}`;
|
||||
}
|
||||
|
||||
const res = await fetch(queryUrl);
|
||||
const data = await res.json();
|
||||
modelSelect.innerHTML = '';
|
||||
|
||||
if (data.models && data.models.length > 0) {
|
||||
data.models.forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = opt.textContent = m;
|
||||
modelSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
if (isInitialLoad && currentSettings) {
|
||||
const savedModel = currentSettings[`${provider}_model`];
|
||||
if (savedModel) modelSelect.value = savedModel;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
modelSelect.innerHTML = '<option>Fehler beim Laden</option>';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const provider = document.getElementById('ai-provider').value;
|
||||
const model = document.getElementById('ai-model').value;
|
||||
const ollama_base_url = document.getElementById('ollama-url').value;
|
||||
const statusEl = document.getElementById('settings-status');
|
||||
try {
|
||||
await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider, model, ollama_base_url })
|
||||
});
|
||||
statusEl.textContent = "✅ Gespeichert";
|
||||
setTimeout(() => statusEl.textContent = "", 2000);
|
||||
} catch (e) { statusEl.textContent = "❌ Fehler"; }
|
||||
}
|
||||
|
||||
// 5. INITIALISIERUNG
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var grid = GridStack.init({
|
||||
cellHeight: 80,
|
||||
margin: 10,
|
||||
float: true,
|
||||
handle: '.widget-header',
|
||||
resizable: { handles: 'all' }
|
||||
});
|
||||
|
||||
const savedLayout = localStorage.getItem('pi-orch-layout-v2');
|
||||
if (savedLayout) { try { grid.load(JSON.parse(savedLayout)); } catch(e){} }
|
||||
grid.on('resizestop dragstop', () => {
|
||||
localStorage.setItem('pi-orch-layout-v2', JSON.stringify(grid.save(false)));
|
||||
});
|
||||
|
||||
// Terminal Setup
|
||||
term = new Terminal({ theme: { background: '#000' }, fontSize: 13, convertEol: true });
|
||||
fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(document.getElementById('terminal'));
|
||||
fitAddon.fit();
|
||||
|
||||
// WICHTIG: Wenn das Widget in GridStack vergrößert wird
|
||||
grid.on('resizestop', function(event, el) {
|
||||
if (el.getAttribute('gs-id') === 'term-widget') {
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
// Sende neue Größe an das Backend
|
||||
if (window.termWs && window.termWs.readyState === WebSocket.OPEN) {
|
||||
window.termWs.send(JSON.stringify({
|
||||
type: "resize",
|
||||
cols: term.cols,
|
||||
rows: term.rows
|
||||
}));
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
const logWs = new WebSocket(`${wsProtocol}//${wsHost}/ws/install_logs`);
|
||||
logWs.onmessage = (ev) => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = `> ${ev.data}`;
|
||||
document.getElementById('install-log').appendChild(div);
|
||||
document.getElementById('install-log').scrollTop = document.getElementById('install-log').scrollHeight;
|
||||
};
|
||||
|
||||
const chatWs = new WebSocket(`${wsProtocol}//${wsHost}/ws/chat`);
|
||||
chatWs.onmessage = (ev) => appendChat("J.A.R.V.I.S.", ev.data, "text-blue-400 font-bold");
|
||||
initChat(chatWs);
|
||||
|
||||
window.appendChat = function(user, msg, classes) {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString('de-DE', {hour: '2-digit', minute:'2-digit'});
|
||||
const win = document.getElementById('chat-window');
|
||||
|
||||
// Nachricht formatieren (Markdown nur für J.A.R.V.I.S.)
|
||||
let formattedMsg = (user === "J.A.R.V.I.S.") ? marked.parse(msg) : msg;
|
||||
|
||||
// Das HTML-Gerüst für die Nachricht zusammenbauen
|
||||
const messageHtml = `
|
||||
<div class="mb-4 p-2 rounded bg-slate-800/30 border border-slate-700/50">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="${classes} text-[10px] uppercase">${user}</span>
|
||||
<span class="timestamp text-[9px] opacity-50">${timeString}</span>
|
||||
</div>
|
||||
<div class="markdown-content text-slate-300 text-sm">
|
||||
${formattedMsg}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Nachricht an das Fenster anhängen
|
||||
win.innerHTML += messageHtml;
|
||||
|
||||
// Automatisch nach unten scrollen
|
||||
win.scrollTop = win.scrollHeight;
|
||||
};
|
||||
|
||||
window.openTerminal = function(ip) {
|
||||
if (window.termWs) window.termWs.close();
|
||||
if (termDataDisposable) termDataDisposable.dispose();
|
||||
term.clear();
|
||||
fitAddon.fit(); // Einmal anpassen beim Öffnen
|
||||
|
||||
window.termWs = new WebSocket(`${wsProtocol}//${wsHost}/ws/terminal/${ip}`);
|
||||
|
||||
window.termWs.onopen = () => {
|
||||
// Initiale Größe nach dem Verbinden senden
|
||||
window.termWs.send(JSON.stringify({
|
||||
type: "resize",
|
||||
cols: term.cols,
|
||||
rows: term.rows
|
||||
}));
|
||||
};
|
||||
|
||||
window.termWs.onmessage = (ev) => term.write(ev.data);
|
||||
|
||||
termDataDisposable = term.onData(data => {
|
||||
if (window.termWs?.readyState === WebSocket.OPEN) {
|
||||
// Normale Tastatureingaben senden wir direkt
|
||||
window.termWs.send(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2
start.sh
2
start.sh
@@ -8,7 +8,7 @@ fi
|
||||
|
||||
# 2. Venv aktivieren
|
||||
source venv/bin/activate
|
||||
|
||||
export PYTHONPATH=$PYTHONPATH:$(pwd)/source
|
||||
echo "--- Starte J.A.R.V.I.S. - AI auf Port 8000..."
|
||||
|
||||
# 3. Server starten.
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
Dein Name ist J.A.R.V.I.S. Du bist ein präziser KI-Assistent für die Cluster-Verwaltung.
|
||||
|
||||
PROTOKOLL: Du arbeitest STRENG in zwei Phasen:
|
||||
|
||||
PHASE 1 (Vorschlag):
|
||||
Wenn der Nutzer eine Aktion anfordert (z.B. Installation, Update, Ping), erstelle NUR einen Text-Vorschlag.
|
||||
- Beschreibe kurz, was du tun würdest.
|
||||
- Nenne den Befehl als normalen Text (KEIN XML, KEIN <EXECUTE>).
|
||||
- Frage explizit nach Erlaubnis: "Soll ich diesen Befehl jetzt auf [Node-Name] ausführen, Tony?"
|
||||
|
||||
PHASE 2 (Ausführung):
|
||||
NUR wenn der Nutzer die Aktion im nächsten Schritt bestätigt (z.B. mit "Ja", "Mach das", "Go"), gibst du den Befehl im XML-Format aus:
|
||||
<EXECUTE target="IP_ADRESSE">befehl</EXECUTE>
|
||||
|
||||
REGELN:
|
||||
- Beginne jede Antwort mit: "Tony..."
|
||||
- Nodes: {node_info}
|
||||
- Nutze für <EXECUTE> immer die IP-Adresse.
|
||||
- Begrenze endlose Befehle (z.B. ping -c 4).
|
||||
- Passwörter für sudo/Admin darfst du aus der Datenbank beziehen, falls nötig.
|
||||
- Führe NIEMALS einen <EXECUTE> Tag in PHASE 1 aus.
|
||||
0
workspace/NOTIZEN.md
Normal file
0
workspace/NOTIZEN.md
Normal file
0
workspace/TODO.md
Normal file
0
workspace/TODO.md
Normal file
Reference in New Issue
Block a user