Auto-Sync: 2026-04-09 17:15
This commit is contained in:
parent
f5081cb8b8
commit
be84354360
4 changed files with 353 additions and 8 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# Arakava News — Live State
|
||||
> Auto-generiert: 2026-04-09 16:45
|
||||
> Auto-generiert: 2026-04-09 17:15
|
||||
|
||||
## Service Status
|
||||
| Service | CT | Status |
|
||||
|
|
@ -8,11 +8,11 @@
|
|||
| WordPress Docker | 101 | running |
|
||||
|
||||
## Letzte Feed-Aktivität (Top 5)
|
||||
Antispiegel: 2026-04-09 14:30:42
|
||||
PAZ: 2026-04-09 14:30:21
|
||||
Junge Freiheit: 2026-04-09 14:00:45
|
||||
Dr. Bines Substack: 2026-04-09 14:00:00
|
||||
Tichys Einblick: 2026-04-09 13:30:24
|
||||
Apollo News: 2026-04-09 15:00:33
|
||||
Photon.info (KI-Analyse): 2026-04-09 14:46:35
|
||||
Rubikon.news: 2026-04-09 14:45:29
|
||||
Norbert Häring: 2026-04-09 14:45:29
|
||||
Antispiegel: 2026-04-09 14:30:42
|
||||
|
||||
## Fehler (letzte 24h)
|
||||
- Fehler gesamt: 0
|
||||
|
|
|
|||
345
homelab-ai-bot/model_library_routes.py
Normal file
345
homelab-ai-bot/model_library_routes.py
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
"""Model Library Integration — Telegram-Buttons, Befehle, Watcher-Webhook.
|
||||
|
||||
Wird von telegram_bot.py eingebunden:
|
||||
import model_library_routes
|
||||
model_library_routes.register(app, bot_token, chat_id)
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import requests
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MODEL_LIBRARY_URL = "http://100.116.108.62:8090"
|
||||
_CHAT_ID: int | None = None
|
||||
_APP = None
|
||||
_TG_TOKEN: str | None = None
|
||||
_PENDING_FILE = "/var/cache/model-library-pending.json"
|
||||
|
||||
# --- Persistence for pending notifications ---
|
||||
|
||||
|
||||
def _load_pending() -> list[dict]:
|
||||
try:
|
||||
with open(_PENDING_FILE) as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return []
|
||||
|
||||
|
||||
def _save_pending(items: list[dict]):
|
||||
with open(_PENDING_FILE, "w") as f:
|
||||
json.dump(items, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def _remove_pending(repo_id: str, filename: str):
|
||||
items = [
|
||||
m for m in _load_pending()
|
||||
if not (m.get("repo_id") == repo_id and m.get("filename") == filename)
|
||||
]
|
||||
_save_pending(items)
|
||||
|
||||
|
||||
# --- Telegram handlers ---
|
||||
|
||||
|
||||
async def cmd_modelle(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
"""Zeigt heruntergeladene Modelle aus dem Katalog."""
|
||||
try:
|
||||
r = requests.get(f"{MODEL_LIBRARY_URL}/api/catalog", timeout=10)
|
||||
r.raise_for_status()
|
||||
catalog = r.json()
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"Fehler beim Abruf: {e}")
|
||||
return
|
||||
|
||||
models = catalog.get("models", [])
|
||||
if not models:
|
||||
await update.message.reply_text("📂 Bibliothek ist leer.")
|
||||
return
|
||||
|
||||
try:
|
||||
sr = requests.get(f"{MODEL_LIBRARY_URL}/api/storage", timeout=5)
|
||||
storage = sr.json()
|
||||
header = (
|
||||
f"💾 {storage['used_gb']:.0f} / {storage['total_gb']:.0f} GB "
|
||||
f"({storage['percent_used']:.0f}%) belegt\n\n"
|
||||
)
|
||||
except Exception:
|
||||
header = ""
|
||||
|
||||
cat_groups: dict[str, list] = {}
|
||||
for m in models:
|
||||
cat_groups.setdefault(m.get("category", "llm"), []).append(m)
|
||||
|
||||
cat_icons = {
|
||||
"llm": "🧠", "code": "💻", "vision": "👁", "embedding": "📐",
|
||||
"audio": "🎵", "image-gen": "🎨",
|
||||
}
|
||||
|
||||
lines = [f"📚 *KI-Modell-Bibliothek* ({len(models)} Modelle)\n{header}"]
|
||||
for cat, items in sorted(cat_groups.items()):
|
||||
icon = cat_icons.get(cat, "📦")
|
||||
lines.append(f"{icon} *{cat.upper()}*")
|
||||
for m in sorted(items, key=lambda x: x["filename"]):
|
||||
q = m.get("quant", "?")
|
||||
sz = m.get("size_gb", 0)
|
||||
lines.append(f" • `{m['filename']}` ({q}, {sz:.1f} GB)")
|
||||
lines.append("")
|
||||
|
||||
await update.message.reply_text("\n".join(lines), parse_mode="Markdown")
|
||||
|
||||
|
||||
async def cmd_watchlist(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
"""Zeigt die aktuelle Model-Watchlist."""
|
||||
try:
|
||||
r = requests.get(f"{MODEL_LIBRARY_URL}/api/watchlist", timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"Fehler beim Abruf: {e}")
|
||||
return
|
||||
|
||||
authors = data.get("authors", [])
|
||||
if not authors:
|
||||
await update.message.reply_text("Watchlist ist leer.")
|
||||
return
|
||||
|
||||
lines = ["👀 *Model-Watchlist*\n"]
|
||||
for a in authors:
|
||||
name = a.get("name", "?")
|
||||
comment = a.get("comment", "")
|
||||
lines.append(f" • *{name}* — {comment}")
|
||||
|
||||
await update.message.reply_text("\n".join(lines), parse_mode="Markdown")
|
||||
|
||||
|
||||
def _resolve_callback(data: str) -> tuple[str, str] | None:
|
||||
"""Löst Callback-Data auf — direkt (repo|file) oder über Index (idx_N)."""
|
||||
prefix = data[:9]
|
||||
payload = data[9:]
|
||||
|
||||
if payload.startswith("idx_"):
|
||||
try:
|
||||
idx = int(payload[4:])
|
||||
pending = _load_pending()
|
||||
if 0 <= idx < len(pending):
|
||||
m = pending[idx]
|
||||
return m.get("repo_id"), m.get("filename")
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return None
|
||||
|
||||
if "|" in payload:
|
||||
return tuple(payload.split("|", 1))
|
||||
return None
|
||||
|
||||
|
||||
async def handle_model_callback(data: str, query) -> bool:
|
||||
"""Verarbeitet model_dl_* und model_ig_* Callbacks. Gibt True zurück wenn verarbeitet."""
|
||||
if data.startswith("model_dl_"):
|
||||
resolved = _resolve_callback(data)
|
||||
if not resolved:
|
||||
await query.edit_message_text(query.message.text + "\n\n⚠️ Modell nicht mehr verfügbar")
|
||||
return True
|
||||
repo_id, filename = resolved
|
||||
|
||||
await query.edit_message_text(
|
||||
query.message.text + f"\n\n⏳ Download gestartet: `{filename}`",
|
||||
parse_mode="Markdown",
|
||||
)
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{MODEL_LIBRARY_URL}/api/download",
|
||||
json={"repo_id": repo_id, "filename": filename},
|
||||
timeout=15,
|
||||
)
|
||||
result = r.json()
|
||||
if result.get("status") == "started":
|
||||
await query.edit_message_text(
|
||||
query.message.text.split("\n\n⏳")[0]
|
||||
+ f"\n\n✅ Download läuft: `{filename}`",
|
||||
parse_mode="Markdown",
|
||||
)
|
||||
else:
|
||||
err = result.get("error", "Unbekannter Fehler")
|
||||
await query.edit_message_text(
|
||||
query.message.text.split("\n\n⏳")[0]
|
||||
+ f"\n\n⚠️ {err}",
|
||||
parse_mode="Markdown",
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception("Model download trigger failed")
|
||||
await query.edit_message_text(
|
||||
query.message.text.split("\n\n⏳")[0]
|
||||
+ f"\n\n❌ Fehler: {e}",
|
||||
parse_mode="Markdown",
|
||||
)
|
||||
_remove_pending(repo_id, filename)
|
||||
return True
|
||||
|
||||
if data.startswith("model_ig_"):
|
||||
resolved = _resolve_callback(data)
|
||||
if not resolved:
|
||||
await query.edit_message_text(query.message.text + "\n\n⚠️ Modell nicht mehr verfügbar")
|
||||
return True
|
||||
repo_id, filename = resolved
|
||||
|
||||
try:
|
||||
requests.post(
|
||||
f"{MODEL_LIBRARY_URL}/api/ignore",
|
||||
json={"repo_id": repo_id, "filename": filename},
|
||||
timeout=10,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
await query.edit_message_text(
|
||||
query.message.text + "\n\n🚫 Ignoriert"
|
||||
)
|
||||
_remove_pending(repo_id, filename)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# --- Webhook: Watcher meldet neue Modelle ---
|
||||
|
||||
|
||||
def _notify_new_models_sync(models: list[dict]):
|
||||
"""Sendet Telegram-Nachrichten mit Inline-Buttons fuer jedes neue Modell.
|
||||
|
||||
Nutzt die Telegram HTTP API direkt (nicht python-telegram-bot),
|
||||
da wir in einem Worker-Thread ohne asyncio-Loop laufen.
|
||||
"""
|
||||
if not _TG_TOKEN or not _CHAT_ID:
|
||||
log.warning("Bot-Token oder Chat-ID nicht konfiguriert")
|
||||
return
|
||||
|
||||
MAX_PER_BATCH = 10
|
||||
if len(models) > MAX_PER_BATCH:
|
||||
log.warning("Benachrichtigungen begrenzt: %d -> %d", len(models), MAX_PER_BATCH)
|
||||
models = models[:MAX_PER_BATCH]
|
||||
|
||||
pending = _load_pending()
|
||||
sent = 0
|
||||
|
||||
for m in models:
|
||||
repo_id = m.get("repo_id", "?")
|
||||
filename = m.get("filename", "?")
|
||||
size_gb = m.get("size_gb", 0)
|
||||
quant = m.get("quant", "?")
|
||||
|
||||
already = any(
|
||||
p.get("repo_id") == repo_id and p.get("filename") == filename
|
||||
for p in pending
|
||||
)
|
||||
if already:
|
||||
continue
|
||||
|
||||
cb_dl = f"model_dl_{repo_id}|{filename}"
|
||||
cb_ig = f"model_ig_{repo_id}|{filename}"
|
||||
|
||||
used_index = False
|
||||
if len(cb_dl.encode()) > 64 or len(cb_ig.encode()) > 64:
|
||||
pending.append(m)
|
||||
idx = len(pending) - 1
|
||||
_save_pending(pending)
|
||||
used_index = True
|
||||
cb_dl = f"model_dl_idx_{idx}"
|
||||
cb_ig = f"model_ig_idx_{idx}"
|
||||
|
||||
safe_repo = repo_id.replace("_", "\\_")
|
||||
text = (
|
||||
f"🆕 *Neues KI-Modell verfügbar*\n\n"
|
||||
f"📦 `{filename}`\n"
|
||||
f"🏷 Repo: {safe_repo}\n"
|
||||
f"📏 {size_gb:.1f} GB | Quant: {quant}"
|
||||
)
|
||||
|
||||
keyboard = {
|
||||
"inline_keyboard": [[
|
||||
{"text": "✅ Herunterladen", "callback_data": cb_dl},
|
||||
{"text": "🚫 Ignorieren", "callback_data": cb_ig},
|
||||
]]
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"https://api.telegram.org/bot{_TG_TOKEN}/sendMessage",
|
||||
json={
|
||||
"chat_id": _CHAT_ID,
|
||||
"text": text,
|
||||
"parse_mode": "Markdown",
|
||||
"reply_markup": keyboard,
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
if resp.ok:
|
||||
log.info("Modell-Benachrichtigung gesendet: %s", filename)
|
||||
sent += 1
|
||||
else:
|
||||
log.error("Telegram API Fehler: %s %s", resp.status_code, resp.text[:200])
|
||||
except Exception:
|
||||
log.exception("Fehler beim Senden der Modell-Benachrichtigung: %s", filename)
|
||||
|
||||
if not used_index:
|
||||
pending.append(m)
|
||||
_save_pending(pending)
|
||||
time.sleep(1.5)
|
||||
|
||||
|
||||
def start_webhook_listener():
|
||||
"""Startet einen kleinen Flask-Server (Port 8767) fuer Watcher-Webhooks."""
|
||||
from flask import Flask, request as freq, jsonify
|
||||
|
||||
webhook_app = Flask("model-library-webhook")
|
||||
webhook_app.logger.setLevel(logging.WARNING)
|
||||
werkzeug_log = logging.getLogger("werkzeug")
|
||||
werkzeug_log.setLevel(logging.WARNING)
|
||||
|
||||
@webhook_app.route("/api/notify_new_models", methods=["POST"])
|
||||
def notify():
|
||||
data = freq.get_json(silent=True)
|
||||
if not data:
|
||||
return jsonify({"error": "no data"}), 400
|
||||
models = data.get("models", [])
|
||||
if models:
|
||||
threading.Thread(
|
||||
target=_notify_new_models_sync, args=(models,), daemon=True
|
||||
).start()
|
||||
return jsonify({"status": "ok", "queued": len(models)})
|
||||
|
||||
@webhook_app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
t = threading.Thread(
|
||||
target=lambda: webhook_app.run(host="0.0.0.0", port=8767, use_reloader=False),
|
||||
daemon=True,
|
||||
)
|
||||
t.start()
|
||||
log.info("Model-Library Webhook-Listener auf Port 8767 gestartet")
|
||||
|
||||
|
||||
# --- Registration ---
|
||||
|
||||
|
||||
def register(app, chat_id: int | None, token: str | None = None):
|
||||
"""Wird von telegram_bot.py aufgerufen."""
|
||||
global _CHAT_ID, _APP, _TG_TOKEN
|
||||
_CHAT_ID = chat_id
|
||||
_APP = app
|
||||
_TG_TOKEN = token
|
||||
|
||||
from telegram.ext import CommandHandler
|
||||
app.add_handler(CommandHandler("modelle", cmd_modelle))
|
||||
app.add_handler(CommandHandler("watchlist", cmd_watchlist))
|
||||
|
||||
start_webhook_listener()
|
||||
log.info("Model-Library Routes registriert (/modelle, /watchlist, Webhook :8767)")
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# Infrastruktur — Live State
|
||||
> Auto-generiert: 2026-04-09 16:45
|
||||
> Auto-generiert: 2026-04-09 17:15
|
||||
|
||||
## pve-hetzner Disk
|
||||
| Mount | Belegt |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Smart Home Muldenstein — Live State
|
||||
> Auto-generiert: 2026-04-09 16:45
|
||||
> Auto-generiert: 2026-04-09 17:15
|
||||
|
||||
## Backup-Status
|
||||
- Letztes Backup: 656MB, 2026-04-09 04:01
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue