v0.5.0 – Clownfischserver HomeIP Auto-Update: myfritz Automatik für SSH (5-Min Cron)

Features:
- install.sh: Frage in Step 4 ob myfritz-Adresse für Auto-SSH-Freigabe genutzt werden soll
- clownfischserver-homeip-updater.sh: DNS-Auflösung + UFW-Update (alle 5 Min via Cron)
- .env-Variable: CLOWNFISCHSERVER_HOME_IP_DOMAIN für myfritz-Adresse
- State-File tracking zur Vermeidung unnötiger UFW-Updates
- Logging: /var/log/clownfischserver-homeip.log
- Dokumentation: scripts/README-HOMEIP.md + CLOWNFISCHSERVER.md update

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 23:28:36 +02:00
co-authored by Claude Haiku 4.5
commit eff7d0d98b
23 changed files with 4143 additions and 0 deletions
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""
🐠 Clownfischserver – OpenRouter Free Models Fallback
Author: Mehlzoerwer-Claude (https://github.com/mehlzoerwer-claude)
License: GPL-3.0 – Keep it open. Always.
Repo: https://github.com/mehlzoerwer-claude/clownfischserver
"""
import json
import logging
import os
import re
import asyncio
import requests
from typing import Optional
logger = logging.getLogger(__name__)
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
# Free models on OpenRouter (no credit card needed)
# openrouter/free auto-selects the best available free model
FREE_MODELS = {
"coder": "openrouter/free",
"chat": "openrouter/free",
}
def _extract_json(raw: str) -> dict:
"""Extract JSON from response."""
text = raw.strip()
text = re.sub(r"^```(?:json)?\s*", "", text)
text = re.sub(r"\s*```$", "", text)
text = text.strip()
try:
return json.loads(text)
except json.JSONDecodeError:
pass
match = re.search(r"\{[^{}]*\}", text, re.DOTALL)
if match:
try:
return json.loads(match.group())
except json.JSONDecodeError:
pass
raise json.JSONDecodeError("No valid JSON found", text, 0)
class OpenRouterClient:
"""OpenRouter Free Models Client – fallback when Ollama times out."""
def __init__(self):
self.api_key = OPENROUTER_API_KEY
self.base_url = OPENROUTER_BASE_URL
if self.api_key:
logger.info("OpenRouter Fallback konfiguriert")
else:
logger.info("OpenRouter nicht konfiguriert – Fallback deaktiviert")
def is_available(self) -> bool:
return bool(self.api_key)
def _chat_sync(self, messages: list, system: Optional[str] = None, model: str = None) -> str:
"""Synchronous OpenRouter API call."""
if not self.api_key:
raise RuntimeError("OpenRouter API Key nicht konfiguriert")
model = model or FREE_MODELS["chat"]
api_messages = []
if system:
api_messages.append({"role": "system", "content": system})
api_messages.extend(messages)
try:
response = requests.post(
f"{self.base_url}/chat/completions",
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
},
json={
"model": model,
"messages": api_messages,
"temperature": 0.7,
},
timeout=60,
)
response.raise_for_status()
data = response.json()
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
except requests.exceptions.ConnectionError:
raise RuntimeError("OpenRouter nicht erreichbar")
except requests.exceptions.Timeout:
raise RuntimeError("OpenRouter Timeout")
except Exception as e:
raise RuntimeError(f"OpenRouter Fehler: {e}")
async def generate_shell_command(self, description: str) -> dict:
"""Generate shell command via OpenRouter."""
loop = asyncio.get_running_loop()
prompt = (
"You are a Linux bash command generator for Ubuntu 24.04.\n"
"Convert the request into a single non-interactive bash command.\n"
"Respond ONLY with JSON: {\"dangerous\": bool, \"reason\": string, \"command\": string}\n"
"No interactive commands (nano, vim, crontab -e). Use sudo when needed.\n\n"
f"Request: {description}\n\nJSON only:"
)
try:
raw = await loop.run_in_executor(
None,
lambda: self._chat_sync(
messages=[{"role": "user", "content": prompt}],
model=FREE_MODELS["coder"],
),
)
result = _extract_json(raw)
if "command" not in result:
result["command"] = ""
if "dangerous" not in result:
result["dangerous"] = False
logger.info(f"OpenRouter shell: {result}")
return result
except json.JSONDecodeError as e:
logger.warning(f"OpenRouter JSON error: {e}")
return {"dangerous": False, "reason": "", "command": ""}
except Exception as e:
logger.error(f"OpenRouter shell failed: {e}")
raise
async def chat(self, message: str, history: list = None) -> str:
"""General chat via OpenRouter."""
loop = asyncio.get_running_loop()
messages = list(history) if history else []
messages.append({"role": "user", "content": message})
system = (
"Du bist der Clownfischserver KI-Assistent. Du hilfst beim Verwalten eines Linux-Servers.\n"
"Antworte kurz und technisch. Antworte IMMER in der Sprache in der der Nutzer schreibt."
)
try:
return await loop.run_in_executor(
None,
lambda: self._chat_sync(
messages=messages,
system=system,
model=FREE_MODELS["chat"],
),
)
except Exception as e:
return f"OpenRouter Fehler: {e}"
async def explain_output(self, command: str, output: str, error: str = "") -> str:
"""Explain command output via OpenRouter."""
loop = asyncio.get_running_loop()
content = f"Command: {command}\nOutput:\n{output}"
if error:
content += f"\nErrors:\n{error}"
content += "\n\nErkläre kurz was passiert ist und ob es Probleme gibt. Antworte in der gleichen Sprache wie der Nutzer."
try:
return await loop.run_in_executor(
None,
lambda: self._chat_sync(
messages=[{"role": "user", "content": content}],
model=FREE_MODELS["chat"],
),
)
except Exception as e:
logger.error(f"OpenRouter explain failed: {e}")
return ""