Compare commits
3 Commits
dev
...
36eabb27d0
| Author | SHA1 | Date | |
|---|---|---|---|
| 36eabb27d0 | |||
| 4fe8d6106a | |||
| 0c8ac64fc6 |
@@ -1,5 +1,5 @@
|
|||||||
# --- KI Provider Auswahl (google, openai, oder ollama) ---
|
# --- KI Provider Auswahl (google, openai, oder ollama) ---
|
||||||
AI_PROVIDER='nvidia'
|
AI_PROVIDER=google
|
||||||
|
|
||||||
# --- API Keys ---
|
# --- API Keys ---
|
||||||
GOOGLE_API_KEY=dein-google-key-hier
|
GOOGLE_API_KEY=dein-google-key-hier
|
||||||
@@ -12,10 +12,8 @@ OPENAI_MODEL=gpt-4o
|
|||||||
OLLAMA_MODEL=llama3
|
OLLAMA_MODEL=llama3
|
||||||
|
|
||||||
# --- Lokale KI (Ollama) ---
|
# --- Lokale KI (Ollama) ---
|
||||||
OLLAMA_BASE_URL='http://192.168.178.118:11434/v1'
|
OLLAMA_BASE_URL=http://127.0.0.1:11434/v1
|
||||||
|
|
||||||
TELEGRAM_BOT_TOKEN=dein-telegram-bot-token
|
TELEGRAM_BOT_TOKEN=dein-telegram-bot-token
|
||||||
ALLOWED_TELEGRAM_USER_ID=deine-telegram-id
|
ALLOWED_TELEGRAM_USER_ID=deine-telegram-id
|
||||||
|
|
||||||
WEB_USER_NAME=Tony
|
|
||||||
NVIDIA_MODEL='moonshotai/kimi-k2.5'
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
config/.env
|
|
||||||
data/*.db
|
|
||||||
workspace/
|
|
||||||
*/.gitkeep
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
# J.A.R.V.I.S. - AI
|
# J.A.R.V.I.S. - AI
|
||||||
|
|
||||||
### Setup:
|
Setup:
|
||||||
|
curl -sSL https://git.pi-farm.de/pi-farm/PiDoBot/raw/branch/main/setup.sh | bash
|
||||||
|
|
||||||
```
|
|
||||||
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,6 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -9,8 +9,6 @@ import re
|
|||||||
import httpx
|
import httpx
|
||||||
import struct
|
import struct
|
||||||
import termios
|
import termios
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
from telegram import Update
|
from telegram import Update
|
||||||
from telegram.ext import ApplicationBuilder, ContextTypes, MessageHandler, filters
|
from telegram.ext import ApplicationBuilder, ContextTypes, MessageHandler, filters
|
||||||
from telegram.error import InvalidToken
|
from telegram.error import InvalidToken
|
||||||
@@ -22,53 +20,20 @@ from fastapi.responses import RedirectResponse
|
|||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from dotenv import load_dotenv, set_key
|
from dotenv import load_dotenv, set_key
|
||||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
# Basis-Verzeichnis (source/)
|
# Lade Umgebungsvariablen
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
load_dotenv()
|
||||||
|
|
||||||
# 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()
|
app = FastAPI()
|
||||||
#static_path = os.path.join(os.path.dirname(__file__), "static")
|
static_path = os.path.join(os.path.dirname(__file__), "static")
|
||||||
templates = Jinja2Templates(directory=BASE_DIR / "templates")
|
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||||
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
SSH_KEY = os.path.expanduser("~/.ssh/id_rsa")
|
SSH_KEY = os.path.expanduser("~/.ssh/id_rsa")
|
||||||
# Ein Dictionary für verschiedene Chat-Historien
|
DB_PATH = "cluster.db"
|
||||||
chat_histories = {
|
chat_history = []
|
||||||
"private": [] # Hier landen Webchat UND private Telegram-Nachrichten
|
PROMPT_FILE = "system_prompt.txt"
|
||||||
}
|
ENV_FILE = os.path.join(os.path.dirname(__file__), ".env")
|
||||||
|
|
||||||
# KI KONFIGURATION
|
# KI KONFIGURATION
|
||||||
AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower()
|
AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower()
|
||||||
@@ -114,52 +79,37 @@ def get_db():
|
|||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
def get_system_prompt(current_user=WEB_USER_NAME, is_admin=False):
|
def get_system_prompt():
|
||||||
# Entscheide, welche Datei geladen wird
|
conn = get_db()
|
||||||
if is_admin:
|
nodes = conn.execute('SELECT * FROM nodes').fetchall()
|
||||||
prompt_path = CONFIG_DIR / "system_prompt.txt"
|
conn.close()
|
||||||
else:
|
|
||||||
prompt_path = CONFIG_DIR / "group_prompt.txt"
|
|
||||||
|
|
||||||
# Datei auslesen
|
|
||||||
if prompt_path.exists():
|
|
||||||
prompt = prompt_path.read_text(encoding="utf-8")
|
|
||||||
else:
|
|
||||||
# Fallback, falls die Datei fehlt
|
|
||||||
prompt = f"Hallo {current_user}, ich bin dein Assistent."
|
|
||||||
|
|
||||||
# Namen ersetzen (funktioniert in beiden Prompts)
|
|
||||||
prompt = prompt.replace("{user_name}", current_user)
|
|
||||||
|
|
||||||
# Server-Infos NUR für Admins einfügen
|
|
||||||
if is_admin:
|
|
||||||
conn = get_db()
|
|
||||||
nodes = conn.execute('SELECT * FROM nodes').fetchall()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
node_info = ""
|
|
||||||
for n in nodes:
|
|
||||||
docker_str = "Ja" if n['docker_installed'] else "Nein"
|
|
||||||
node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}, OS: {n['os']}, Arch: {n['arch']}, Docker: {docker_str}\n"
|
|
||||||
|
|
||||||
prompt = prompt.replace("{node_info}", node_info)
|
|
||||||
prompt = prompt.replace("{workspace_dir}", str(WORKSPACE_DIR))
|
|
||||||
prompt = prompt.replace("{notes_file}", str(NOTES_FILE))
|
|
||||||
prompt = prompt.replace("{todo_file}", str(TODO_FILE))
|
|
||||||
|
|
||||||
return prompt
|
node_info = ""
|
||||||
|
for n in nodes:
|
||||||
|
docker_str = "Ja" if n['docker_installed'] else "Nein"
|
||||||
|
node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}, OS: {n['os']}, Arch: {n['arch']}, Docker: {docker_str}\n"
|
||||||
|
|
||||||
|
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.")
|
||||||
|
|
||||||
|
return template.replace("{node_info}", node_info)
|
||||||
|
|
||||||
# --- KI FUNKTIONEN ---
|
# --- KI FUNKTIONEN ---
|
||||||
|
|
||||||
async def get_ai_response(user_msg, system_prompt, history_list):
|
async def get_ai_response(user_input, system_prompt):
|
||||||
|
global chat_history
|
||||||
|
chat_history.append({"role": "user", "content": user_input})
|
||||||
|
chat_history = chat_history[-30:]
|
||||||
ai_msg = ""
|
ai_msg = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Kombinierte Logik für OpenAI, Ollama und NVIDIA (alle nutzen das OpenAI SDK)
|
# Kombinierte Logik für OpenAI, Ollama und NVIDIA (alle nutzen das OpenAI SDK)
|
||||||
if AI_PROVIDER in ["openai", "ollama", "nvidia"]:
|
if AI_PROVIDER in ["openai", "ollama", "nvidia"]:
|
||||||
# WICHTIG: Wir nutzen die übergebene 'history_list', NICHT starr "private"
|
messages = [{"role": "system", "content": system_prompt}] + chat_history
|
||||||
# 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":
|
if AI_PROVIDER == "ollama":
|
||||||
url = OLLAMA_BASE_URL
|
url = OLLAMA_BASE_URL
|
||||||
@@ -176,6 +126,7 @@ async def get_ai_response(user_msg, system_prompt, history_list):
|
|||||||
key = OPENAI_API_KEY
|
key = OPENAI_API_KEY
|
||||||
model_to_use = OPENAI_MODEL
|
model_to_use = OPENAI_MODEL
|
||||||
|
|
||||||
|
# WICHTIG: Hier .AsyncOpenAI nutzen, da die Funktion async ist
|
||||||
client = openai.AsyncOpenAI(base_url=url, api_key=key)
|
client = openai.AsyncOpenAI(base_url=url, api_key=key)
|
||||||
response = await client.chat.completions.create(
|
response = await client.chat.completions.create(
|
||||||
model=model_to_use,
|
model=model_to_use,
|
||||||
@@ -184,36 +135,40 @@ async def get_ai_response(user_msg, system_prompt, history_list):
|
|||||||
ai_msg = response.choices[0].message.content
|
ai_msg = response.choices[0].message.content
|
||||||
|
|
||||||
elif AI_PROVIDER == "google":
|
elif AI_PROVIDER == "google":
|
||||||
|
# Für Google Gemini
|
||||||
if not GOOGLE_API_KEY:
|
if not GOOGLE_API_KEY:
|
||||||
return "Fehler: GOOGLE_API_KEY fehlt in der .env Datei!"
|
return "Fehler: GOOGLE_API_KEY fehlt in der .env Datei!"
|
||||||
|
|
||||||
client = genai.Client(api_key=GOOGLE_API_KEY)
|
client = genai.Client(api_key=GOOGLE_API_KEY)
|
||||||
|
|
||||||
|
# Wir müssen unser Array in das spezielle Google-Format umwandeln
|
||||||
google_history = []
|
google_history = []
|
||||||
|
|
||||||
# Alle Nachrichten AUSSER der allerletzten (das ist die aktuelle Frage von gerade eben)
|
# Alle Nachrichten AUSSER der allerletzten (die aktuelle User-Frage) in die History packen
|
||||||
for msg in history_list[:-1]:
|
for msg in chat_history[:-1]:
|
||||||
role = "user" if msg["role"] == "user" else "model"
|
role = "user" if msg["role"] == "user" else "model"
|
||||||
google_history.append(
|
google_history.append(
|
||||||
types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])])
|
types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Chat MIT dem übersetzten Gedächtnis starten
|
||||||
chat = client.chats.create(
|
chat = client.chats.create(
|
||||||
model=GOOGLE_MODEL,
|
model=GOOGLE_MODEL,
|
||||||
config=types.GenerateContentConfig(system_instruction=system_prompt),
|
config=types.GenerateContentConfig(system_instruction=system_prompt),
|
||||||
history=google_history
|
history=google_history
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sende die aktuelle Nachricht an den Chat
|
# Jetzt erst die neue Nachricht an den Chat mit Gedächtnis schicken
|
||||||
response = chat.send_message(user_msg)
|
response = chat.send_message(user_input)
|
||||||
ai_msg = response.text
|
ai_msg = response.text
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
ai_msg = f"Fehler bei der KI-Anfrage: {e}"
|
ai_msg = f"Fehler bei der KI-Anfrage: {e}"
|
||||||
print(f"KI Fehler: {e}")
|
print(f"KI Fehler: {e}")
|
||||||
|
|
||||||
# Wir geben nur den Text zurück. Das Speichern der KI-Antwort in die Historie
|
# 3. Die Antwort der KI ebenfalls ins Gedächtnis aufnehmen
|
||||||
# erledigt der Aufrufer (Web-Chat Endpoint oder Telegram Funktion)!
|
chat_history.append({"role": "assistant", "content": ai_msg})
|
||||||
|
|
||||||
return ai_msg
|
return ai_msg
|
||||||
|
|
||||||
async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
@@ -223,62 +178,30 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_
|
|||||||
chat_type = update.effective_chat.type
|
chat_type = update.effective_chat.type
|
||||||
user_msg = update.message.text
|
user_msg = update.message.text
|
||||||
user_id = str(update.message.from_user.id)
|
user_id = str(update.message.from_user.id)
|
||||||
chat_id = str(update.effective_chat.id)
|
|
||||||
|
|
||||||
# 1. Weiche für Gruppen-Logik & Historien-Zuordnung
|
# 1. Gruppen-Logik: Nur reagieren, wenn der Bot @erwähnt wird
|
||||||
if chat_type in ['group', 'supergroup']:
|
if chat_type in ['group', 'supergroup']:
|
||||||
if bot_username not in user_msg:
|
if bot_username not in user_msg:
|
||||||
return # Bot wurde nicht erwähnt, Nachricht ignorieren
|
return # Bot wurde nicht erwähnt, Nachricht ignorieren
|
||||||
|
|
||||||
# Den @Namen aus dem Text entfernen
|
# Den @Namen aus dem Text entfernen, damit die KI nicht verwirrt wird
|
||||||
user_msg = user_msg.replace(bot_username, "").strip()
|
user_msg = user_msg.replace(bot_username, "").strip()
|
||||||
|
|
||||||
# Jede Gruppe bekommt ihr komplett eigenes Gedächtnis
|
|
||||||
history_key = chat_id
|
|
||||||
else:
|
else:
|
||||||
# Sicherheitscheck im Einzelchat
|
# Im Einzelchat kann optional weiterhin nur der Admin zugelassen werden.
|
||||||
|
# Wenn du willst, dass auch andere den Bot privat nutzen können, entferne diesen Block:
|
||||||
if user_id != ALLOWED_ID:
|
if user_id != ALLOWED_ID:
|
||||||
await update.message.reply_text("Zugriff auf den privaten Chat verweigert. 🔒")
|
await update.message.reply_text("Zugriff auf den privaten Chat verweigert. 🔒")
|
||||||
return
|
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
|
# Tipp-Status anzeigen
|
||||||
await update.message.reply_chat_action(action="typing")
|
await update.message.reply_chat_action(action="typing")
|
||||||
|
|
||||||
# 2. KI fragen (Wir übergeben die spezifische Historie!)
|
# 2. KI fragen
|
||||||
# HINWEIS: Stelle sicher, dass deine get_ai_response Funktion die current_history auch annimmt.
|
ai_response = await get_ai_response(user_msg, get_system_prompt())
|
||||||
# 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)
|
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()
|
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
|
# KI Text-Antwort senden
|
||||||
if clean_msg:
|
if clean_msg:
|
||||||
await update.message.reply_text(clean_msg)
|
await update.message.reply_text(clean_msg)
|
||||||
@@ -300,7 +223,7 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_
|
|||||||
if n:
|
if n:
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_shell(
|
proc = await asyncio.create_subprocess_shell(
|
||||||
f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR {n['user']}@{n['ip']} '{cmd}'",
|
f"ssh -o StrictHostKeyChecking=no {n['user']}@{n['ip']} '{cmd}'",
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.STDOUT
|
stderr=asyncio.subprocess.STDOUT
|
||||||
)
|
)
|
||||||
@@ -310,18 +233,12 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_
|
|||||||
result_text = output[:4000] if output else "✅ Befehl ohne Output ausgeführt."
|
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')
|
await update.message.reply_text(f"💻 **Output von {n['name']}:**\n```\n{result_text}\n```", parse_mode='Markdown')
|
||||||
|
|
||||||
# System-Output auch in die RICHTIGE Historie packen, damit die KI weiß, was passiert ist
|
chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {target} fertig:\n{result_text}"})
|
||||||
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:
|
except Exception as e:
|
||||||
await update.message.reply_text(f"❌ Fehler bei der Ausführung: {e}")
|
await update.message.reply_text(f"❌ Fehler bei der Ausführung: {e}")
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text(f"⚠️ Node '{target}' nicht in der Datenbank gefunden.")
|
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) ---
|
# --- FASTAPI LIFESPAN EVENTS (Bot starten/stoppen) ---
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
@@ -373,7 +290,7 @@ async def get_remote_info(ip, user):
|
|||||||
"""Versucht Linux/Mac-Infos zu lesen, falls fehlgeschlagen, dann Windows."""
|
"""Versucht Linux/Mac-Infos zu lesen, falls fehlgeschlagen, dann Windows."""
|
||||||
# 1. Versuch: Linux/Mac
|
# 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)"
|
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 LogLevel=ERROR -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{linux_cmd}\""
|
ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{linux_cmd}\""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(ssh_cmd, shell=True, stderr=subprocess.DEVNULL).decode().strip().split('\n')
|
output = subprocess.check_output(ssh_cmd, shell=True, stderr=subprocess.DEVNULL).decode().strip().split('\n')
|
||||||
@@ -389,7 +306,7 @@ async def get_remote_info(ip, user):
|
|||||||
# 2. Versuch: Windows (CMD)
|
# 2. Versuch: Windows (CMD)
|
||||||
# ver = OS Version, echo %PROCESSOR_ARCHITECTURE% = Arch, where docker = Docker Check
|
# 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)'
|
win_cmd = 'ver && echo %PROCESSOR_ARCHITECTURE% && (where docker >nul 2>&1 && echo 1 || echo 0)'
|
||||||
ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{win_cmd}\""
|
ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} \"{win_cmd}\""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(ssh_cmd, shell=True).decode().strip().split('\n')
|
output = subprocess.check_output(ssh_cmd, shell=True).decode().strip().split('\n')
|
||||||
@@ -424,7 +341,7 @@ async def bootstrap_node(ip, user, password):
|
|||||||
cmd_universal = f'mkdir .ssh & echo {pub_key} >> .ssh/authorized_keys'
|
cmd_universal = f'mkdir .ssh & echo {pub_key} >> .ssh/authorized_keys'
|
||||||
|
|
||||||
# sshpass direkt mit dem simplen Befehl
|
# sshpass direkt mit dem simplen Befehl
|
||||||
setup_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null {user}@{ip} \"{cmd_universal}\""
|
setup_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {user}@{ip} \"{cmd_universal}\""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Wir führen es aus. Das "2x Passwort"-Problem kommt oft von TTY-Anfragen.
|
# Wir führen es aus. Das "2x Passwort"-Problem kommt oft von TTY-Anfragen.
|
||||||
@@ -637,80 +554,36 @@ async def terminal_websocket(websocket: WebSocket, ip: str):
|
|||||||
@app.websocket("/ws/chat")
|
@app.websocket("/ws/chat")
|
||||||
async def chat_endpoint(websocket: WebSocket):
|
async def chat_endpoint(websocket: WebSocket):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
# Sicherstellen, dass die private History existiert
|
|
||||||
if "private" not in chat_histories:
|
|
||||||
chat_histories["private"] = []
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
user_msg = await websocket.receive_text()
|
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)
|
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()
|
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:
|
if commands:
|
||||||
tasks = []
|
tasks = []
|
||||||
for target, cmd in commands:
|
for target, cmd in commands:
|
||||||
conn = get_db()
|
conn = get_db(); n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone(); conn.close()
|
||||||
n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone()
|
if n: tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip()))
|
||||||
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:
|
if tasks:
|
||||||
await websocket.send_text("ℹ️ *Führe Befehle aus...*")
|
await websocket.send_text("ℹ️ *Führe Befehle aus...*")
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
# Nochmal KI fragen für Zusammenfassung (optional)
|
summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt())
|
||||||
summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt(), chat_histories["private"])
|
|
||||||
await websocket.send_text(f"--- Info ---\n{summary}")
|
await websocket.send_text(f"--- Info ---\n{summary}")
|
||||||
except WebSocketDisconnect:
|
except: pass
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Webchat Fehler: {e}")
|
|
||||||
|
|
||||||
async def run_remote_task(ip, user, cmd, history_key="private"): # history_key als Parameter
|
async def run_remote_task(ip, user, cmd):
|
||||||
await manager.broadcast(f"🚀 Task: {cmd} auf {ip}")
|
await manager.broadcast(f"🚀 Task: {cmd} auf {ip}")
|
||||||
proc = await asyncio.create_subprocess_shell(
|
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)
|
||||||
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 = ""
|
full_output = ""
|
||||||
while True:
|
while True:
|
||||||
line = await proc.stdout.readline()
|
line = await proc.stdout.readline()
|
||||||
if not line: break
|
if not line: break
|
||||||
out = line.decode('utf-8', errors='ignore').strip()
|
out = line.decode('utf-8', errors='ignore').strip()
|
||||||
if out:
|
if out: await manager.broadcast(f"🛠️ {out}"); full_output += out + "\n"
|
||||||
await manager.broadcast(f"🛠️ {out}")
|
|
||||||
full_output += out + "\n"
|
|
||||||
await proc.wait()
|
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 ---
|
# --- Settings API ---
|
||||||
@app.get("/api/settings")
|
@app.get("/api/settings")
|
||||||
@@ -820,19 +693,6 @@ async def get_models(provider: str, url: str = None):
|
|||||||
print(f"Fehler beim Abrufen der Modelle für {provider}: {str(e)}")
|
print(f"Fehler beim Abrufen der Modelle für {provider}: {str(e)}")
|
||||||
return {"models": []} # Gibt eine leere Liste zurück -> Frontend nutzt Fallback
|
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")
|
@app.get("/debug_keys")
|
||||||
async def debug_keys():
|
async def debug_keys():
|
||||||
return {
|
return {
|
||||||
296
setup.sh
Executable file → Normal file
296
setup.sh
Executable file → Normal file
@@ -1,31 +1,27 @@
|
|||||||
#!/bin/bash
|
#!/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"
|
REPO_URL="https://git.pi-farm.de/pi-farm/PiDoBot.git"
|
||||||
SERVICE_FILE="/etc/systemd/system/jarvis.service"
|
INSTALL_DIR="jarvis-ai"
|
||||||
|
|
||||||
echo -e "${C_CYAN}${C_BOLD}==========================================${C_DEF}"
|
echo ">>> Starte Setup für J.A.R.V.I.S. - AI ..."
|
||||||
echo -e "${C_CYAN}${C_BOLD}>>> J.A.R.V.I.S.-AI - Setup <<<${C_DEF}"
|
|
||||||
echo -e "${C_CYAN}${C_BOLD}==========================================${C_DEF}"
|
|
||||||
|
|
||||||
# 0. Installationsverzeichnis abfragen
|
# 1. Prüfen, ob Git installiert ist, ansonsten installieren
|
||||||
read -p "${C_CYAN}Installationsverzeichnis (Standard: ${C_YELLOW}/home/pi/jarvis-ai${C_CYAN}): ${C_DEF}" input_dir </dev/tty
|
if ! command -v git &> /dev/null; then
|
||||||
INSTALL_DIR=${input_dir:-/home/pi/jarvis-ai}
|
echo "--- Git nicht gefunden. Installiere Git..."
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y git
|
||||||
|
else
|
||||||
|
echo "--- Git ist bereits installiert."
|
||||||
|
fi
|
||||||
|
|
||||||
# Pfad normalisieren
|
# 2. Weitere zwingende System-Abhängigkeiten installieren
|
||||||
INSTALL_DIR=$(realpath -m "$INSTALL_DIR")
|
# 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
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
# 3. Repository klonen
|
# 3. Repository klonen
|
||||||
if [ ! -d "$INSTALL_DIR" ]; then
|
if [ ! -d "$INSTALL_DIR" ]; then
|
||||||
echo "--- Klone Repository von $REPO_URL..."
|
echo "--- Klone Repository von $REPO_URL..."
|
||||||
@@ -35,241 +31,51 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# In das Verzeichnis wechseln
|
# In das Verzeichnis wechseln
|
||||||
=======
|
|
||||||
echo -e "\n${C_GREEN}Zielverzeichnis: ${C_BOLD}$INSTALL_DIR${C_DEF}"
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
|
||||||
>>>>>>> dev
|
|
||||||
cd "$INSTALL_DIR" || exit
|
cd "$INSTALL_DIR" || exit
|
||||||
|
|
||||||
# 1. System-Abhängigkeiten
|
# 4. Virtual Environment und Python-Abhängigkeiten einrichten
|
||||||
echo -e "\n${C_BLUE}${C_BOLD}--- 1. Prüfe und installiere System-Pakete...${C_DEF}"
|
echo "--- Erstelle Python Virtual Environment..."
|
||||||
sudo apt-get update
|
python3 -m venv venv
|
||||||
sudo apt-get install -y git wget sshpass python3-pip python3-venv iproute2
|
source venv/bin/activate
|
||||||
|
|
||||||
# 2. Repository klonen
|
echo "--- Installiere Python-Pakete aus requirements.txt..."
|
||||||
echo -e "\n${C_BLUE}${C_BOLD}--- 2. Hole Quellcode...${C_DEF}"
|
pip install --upgrade pip
|
||||||
if [ ! -d ".git" ]; then
|
if [ -f "requirements.txt" ]; then
|
||||||
git clone --branch dev --single-branch "$REPO_URL" .
|
pip install -r requirements.txt
|
||||||
else
|
else
|
||||||
echo -e "${C_YELLOW}Repo bereits vorhanden, aktualisiere...${C_DEF}"
|
echo "FEHLER: requirements.txt nicht im Repository gefunden!"
|
||||||
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. SSH-Key & Konfiguration (Silent Mode)
|
# 5. SSH-Key für den Master prüfen/erstellen (für passwortlosen Zugriff auf Nodes)
|
||||||
echo -e "\n${C_BLUE}${C_BOLD}--- 4. Prüfe SSH-Schlüssel und Konfiguration...${C_DEF}"
|
|
||||||
if [ ! -f "$HOME/.ssh/id_rsa" ]; then
|
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"
|
ssh-keygen -t rsa -N "" -f "$HOME/.ssh/id_rsa"
|
||||||
echo -e "${C_GREEN}✅ SSH-Key generiert.${C_DEF}"
|
|
||||||
else
|
else
|
||||||
echo -e "${C_GREEN}✅ SSH-Key existiert bereits.${C_DEF}"
|
echo "--- SSH-Key existiert bereits."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# SSH 'Silent Mode' einrichten, um Spam im Log zu verhindern
|
# 6. Static Ordner anlegen und Dateien herunterladen
|
||||||
mkdir -p "$HOME/.ssh"
|
if [ ! -f "./static" ]; then
|
||||||
if [ ! -f "$HOME/.ssh/config" ] || ! grep -q "StrictHostKeyChecking no" "$HOME/.ssh/config"; then
|
echo "--- Static-Ordner anlegen und Dateien herunterladen..."
|
||||||
echo -e "${C_YELLOW}Richte SSH Silent-Mode für lokale Netze ein...${C_DEF}"
|
mkdir -p static
|
||||||
cat <<EOF >> "$HOME/.ssh/config"
|
cd static
|
||||||
|
# GridStack
|
||||||
Host 192.168.* 10.* 172.*
|
wget https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack.min.css
|
||||||
StrictHostKeyChecking no
|
wget https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all.js
|
||||||
UserKnownHostsFile /dev/null
|
# Xterm
|
||||||
LogLevel ERROR
|
wget https://cdn.jsdelivr.net/npm/xterm@5.1.0/css/xterm.css
|
||||||
EOF
|
wget https://cdn.jsdelivr.net/npm/xterm@5.1.0/lib/xterm.js
|
||||||
chmod 600 "$HOME/.ssh/config"
|
# Xterm Fit Addon
|
||||||
fi
|
wget https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.js
|
||||||
|
# marked.js
|
||||||
# 5. Static Dateien
|
wget https://cdn.jsdelivr.net/npm/marked/marked.min.js
|
||||||
echo -e "\n${C_BLUE}${C_BOLD}--- 5. Lade Frontend-Bibliotheken...${C_DEF}"
|
cd ..
|
||||||
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
|
else
|
||||||
|
echo "--- Static-Dateien existieren bereits."
|
||||||
|
fi
|
||||||
|
echo ">>> Installation abgeschlossen!"
|
||||||
|
echo "--- Starte J.A.R.V.I.S. - AI auf Port 8000..."
|
||||||
|
|
||||||
# 7. Port Check
|
# 6. Programm starten
|
||||||
echo -e "\n${C_BLUE}${C_BOLD}--- 7. Prüfe Port 8000...${C_DEF}"
|
python3 -m uvicorn main:app --host 0.0.0.0 --port 8000
|
||||||
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
|
|
||||||
@@ -1,583 +0,0 @@
|
|||||||
<!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
|
# 2. Venv aktivieren
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
export PYTHONPATH=$PYTHONPATH:$(pwd)/source
|
|
||||||
echo "--- Starte J.A.R.V.I.S. - AI auf Port 8000..."
|
echo "--- Starte J.A.R.V.I.S. - AI auf Port 8000..."
|
||||||
|
|
||||||
# 3. Server starten.
|
# 3. Server starten.
|
||||||
|
|||||||
21
system_prompt.txt
Normal file
21
system_prompt.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
Reference in New Issue
Block a user