bookstack_connector.py aktualisiert

This commit is contained in:
2026-03-06 14:24:04 +00:00
parent dc3dcb016d
commit adac7ef7ff

View File

@@ -3,7 +3,7 @@ title: BookStack Connector
description: Connects to a BookStack wiki server via its REST API. Allows the LLM to list, read, create, edit and delete shelves, books, chapters and pages. All capabilities can be individually enabled/disabled by an admin via Valves. description: Connects to a BookStack wiki server via its REST API. Allows the LLM to list, read, create, edit and delete shelves, books, chapters and pages. All capabilities can be individually enabled/disabled by an admin via Valves.
author: Claude / Anthropic author: Claude / Anthropic
author_url: https://claude.ai author_url: https://claude.ai
version: 1.3.0 version: 1.4.0
license: MIT license: MIT
requirements: requests requirements: requests
""" """
@@ -14,13 +14,12 @@ import requests
from typing import Any, Callable, Optional from typing import Any, Callable, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
# Logging-Setup: schreibt in den OWUI-Server-Log (podman logs open-webui)
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger("BookStackConnector") log = logging.getLogger("BookStackConnector")
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Helper: EventEmitter wrapper # Helper: EventEmitter wrapper
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
class EventEmitter: class EventEmitter:
def __init__(self, event_emitter: Callable[[dict], Any] = None): def __init__(self, event_emitter: Callable[[dict], Any] = None):
@@ -46,7 +45,7 @@ class EventEmitter:
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Helper: BookStack API client # Helper: BookStack API client
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
class BookStackAPI: class BookStackAPI:
def __init__(self, base_url: str, token_id: str, token_secret: str): def __init__(self, base_url: str, token_id: str, token_secret: str):
@@ -57,7 +56,6 @@ class BookStackAPI:
} }
def _parse_response(self, resp: requests.Response) -> dict: def _parse_response(self, resp: requests.Response) -> dict:
"""Parst die API-Antwort mit ausführlichem Logging bei Fehlern."""
log.debug( log.debug(
"[BookStack] %s %s → HTTP %s | Content-Type: %s | Body[:300]: %s", "[BookStack] %s %s → HTTP %s | Content-Type: %s | Body[:300]: %s",
resp.request.method, resp.request.method,
@@ -79,10 +77,10 @@ class BookStackAPI:
f"Content-Type: {content_type}\n" f"Content-Type: {content_type}\n"
f"Antwort (Anfang): {resp.text[:300]}\n\n" f"Antwort (Anfang): {resp.text[:300]}\n\n"
f"Mögliche Ursachen:\n" f"Mögliche Ursachen:\n"
f" • BOOKSTACK_URL falsch oder nicht erreichbar\n" f" • BOOKSTACK_URL falsch oder nicht erreichbar\n"
f" • API antwortet mit Login-Seite (HTML) → Token ungültig\n" f" • API antwortet mit Login-Seite (HTML) → Token ungültig\n"
f" • SSL-Fehler oder Redirect\n" f" • SSL-Fehler oder Redirect\n"
f" • URL endet mit '/' → doppeltes '/api/'" f" • URL endet mit '/' → doppeltes '/api/'"
) )
return resp.json() return resp.json()
@@ -128,13 +126,11 @@ class BookStackAPI:
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Main Tool class # Main Tool class
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
class Tools: class Tools:
# ── Admin-Valves ──────────────────────────
class Valves(BaseModel): class Valves(BaseModel):
# --- Authentication ---
BOOKSTACK_URL: str = Field( BOOKSTACK_URL: str = Field(
default="", default="",
description="Base URL deiner BookStack-Instanz, z.B. https://wiki.example.com", description="Base URL deiner BookStack-Instanz, z.B. https://wiki.example.com",
@@ -151,8 +147,6 @@ class Tools:
default=100, default=100,
description="Maximale Anzahl Items pro API-Request (Pagination)", description="Maximale Anzahl Items pro API-Request (Pagination)",
) )
# --- Feature Flags (Lesen) ---
ENABLE_LIST_STRUCTURE: bool = Field( ENABLE_LIST_STRUCTURE: bool = Field(
default=True, default=True,
description="✅ Erlauben: Gesamte Bibliotheksstruktur anzeigen (Shelves → Books → Chapters → Pages)", description="✅ Erlauben: Gesamte Bibliotheksstruktur anzeigen (Shelves → Books → Chapters → Pages)",
@@ -165,8 +159,6 @@ class Tools:
default=True, default=True,
description="✅ Erlauben: Inhalte durchsuchen", description="✅ Erlauben: Inhalte durchsuchen",
) )
# --- Feature Flags (Schreiben: Seiten) ---
ENABLE_CREATE_PAGE: bool = Field( ENABLE_CREATE_PAGE: bool = Field(
default=False, default=False,
description="⚠️ Erlauben: Neue Seiten erstellen", description="⚠️ Erlauben: Neue Seiten erstellen",
@@ -179,8 +171,6 @@ class Tools:
default=False, default=False,
description="🔴 Erlauben: Seiten löschen (UNWIDERRUFLICH!)", description="🔴 Erlauben: Seiten löschen (UNWIDERRUFLICH!)",
) )
# --- Feature Flags (Schreiben: Bücher & Kapitel) ---
ENABLE_CREATE_BOOK: bool = Field( ENABLE_CREATE_BOOK: bool = Field(
default=False, default=False,
description="⚠️ Erlauben: Neue Bücher erstellen", description="⚠️ Erlauben: Neue Bücher erstellen",
@@ -199,40 +189,48 @@ class Tools:
) )
def __init__(self): def __init__(self):
"""
Pflicht-Initialisierung: OWUI braucht self.valves = self.Valves() im __init__.
OWUI setzt danach die gespeicherten Admin-Werte auf self.valves aber nur
wenn das Attribut bereits existiert. Ohne __init__ → 'has no attribute valves'.
"""
self.valves = self.Valves() self.valves = self.Valves()
def _api(self) -> BookStackAPI: def _get_valves(self, __user__: dict) -> "Tools.Valves":
"""
Workaround für OWUI-Bug: Valves werden nicht korrekt in self.valves injiziert.
OWUI übergibt die echten Werte zuverlässig über __user__['valves'].
"""
user_valves = __user__.get("valves") if __user__ else None
if user_valves:
try:
return self.Valves(**user_valves) if isinstance(user_valves, dict) else user_valves
except Exception as e:
log.warning("[BookStack] Valve-Parsing aus __user__ fehlgeschlagen: %s", e)
return self.valves
def _api(self, valves: "Tools.Valves") -> BookStackAPI:
return BookStackAPI( return BookStackAPI(
self.valves.BOOKSTACK_URL, valves.BOOKSTACK_URL,
self.valves.API_TOKEN_ID, valves.API_TOKEN_ID,
self.valves.API_TOKEN_SECRET, valves.API_TOKEN_SECRET,
) )
def _check_config(self) -> Optional[str]: def _check_config(self, valves: "Tools.Valves") -> Optional[str]:
"""Prüft ob Auth-Daten gesetzt sind. Gibt Fehlermeldung zurück oder None.""" tid = valves.API_TOKEN_ID
tid = self.valves.API_TOKEN_ID
log.debug( log.debug(
"[BookStack] Config-Check → URL='%s' | TOKEN_ID='%s' | TOKEN_SECRET='%s'", "[BookStack] Config-Check → URL='%s' | TOKEN_ID='%s' | TOKEN_SECRET='%s'",
self.valves.BOOKSTACK_URL, valves.BOOKSTACK_URL,
(tid[:4] + "***") if tid else "(leer)", (tid[:4] + "***") if tid else "(leer)",
"***" if self.valves.API_TOKEN_SECRET else "(leer)", "***" if valves.API_TOKEN_SECRET else "(leer)",
) )
if not self.valves.BOOKSTACK_URL: if not valves.BOOKSTACK_URL:
return "❌ Bitte BOOKSTACK_URL in den Valve-Einstellungen eintragen." return "❌ Bitte BOOKSTACK_URL in den Valve-Einstellungen eintragen."
if not self.valves.API_TOKEN_ID or not self.valves.API_TOKEN_SECRET: if not valves.API_TOKEN_ID or not valves.API_TOKEN_SECRET:
return "❌ Bitte API_TOKEN_ID und API_TOKEN_SECRET in den Valve-Einstellungen eintragen." return "❌ Bitte API_TOKEN_ID und API_TOKEN_SECRET in den Valve-Einstellungen eintragen."
return None return None
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 1 Gesamtstruktur anzeigen # TOOL 1 Gesamtstruktur anzeigen
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def get_library_structure( async def get_library_structure(
self, self,
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -243,33 +241,33 @@ class Tools:
:return: A formatted Markdown tree of the entire library with IDs. :return: A formatted Markdown tree of the entire library with IDs.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_LIST_STRUCTURE: if not valves.ENABLE_LIST_STRUCTURE:
return "❌ Die Funktion 'Bibliotheksstruktur anzeigen' ist vom Administrator deaktiviert." return "❌ Die Funktion 'Bibliotheksstruktur anzeigen' ist vom Administrator deaktiviert."
err = self._check_config() err = self._check_config(valves)
if err: if err:
return err return err
try: try:
api = self._api() api = self._api(valves)
await emitter.status("📚 Lade Shelves...") await emitter.status("📚 Lade Shelves...")
shelves = api.list_all("shelves", self.valves.MAX_ITEMS) shelves = api.list_all("shelves", valves.MAX_ITEMS)
await emitter.status("📖 Lade Bücher...") await emitter.status("📖 Lade Bücher...")
books = api.list_all("books", self.valves.MAX_ITEMS) books = api.list_all("books", valves.MAX_ITEMS)
await emitter.status("📑 Lade Kapitel...") await emitter.status("📑 Lade Kapitel...")
chapters = api.list_all("chapters", self.valves.MAX_ITEMS) chapters = api.list_all("chapters", valves.MAX_ITEMS)
await emitter.status("📄 Lade Seiten...") await emitter.status("📄 Lade Seiten...")
pages = api.list_all("pages", self.valves.MAX_ITEMS) pages = api.list_all("pages", valves.MAX_ITEMS)
await emitter.status("🗂️ Baue Strukturbaum...") await emitter.status("🗂️ Baue Strukturbaum...")
# Shelf → Book Mapping via Shelf-Detailendpunkt
shelf_book_map: dict = {s["id"]: [] for s in shelves} shelf_book_map: dict = {s["id"]: [] for s in shelves}
for shelf in shelves: for shelf in shelves:
detail = api._get(f"shelves/{shelf['id']}") detail = api._get(f"shelves/{shelf['id']}")
@@ -346,22 +344,9 @@ class Tools:
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") log.error("[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s", e.response.status_code, ct, body)
if e.response is not None msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
@@ -371,12 +356,13 @@ class Tools:
return msg return msg
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 2 Seite lesen # TOOL 2 Seite lesen
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def get_page_content( async def get_page_content(
self, self,
page_id: int, page_id: int,
format: str = "markdown", format: str = "markdown",
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -387,16 +373,17 @@ class Tools:
:param format: Content format 'markdown', 'html', or 'both'. Default: 'markdown'. :param format: Content format 'markdown', 'html', or 'both'. Default: 'markdown'.
:return: The page title and its content in the requested format. :return: The page title and its content in the requested format.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_READ_PAGE: if not valves.ENABLE_READ_PAGE:
return "❌ Die Funktion 'Seite lesen' ist vom Administrator deaktiviert." return "❌ Die Funktion 'Seite lesen' ist vom Administrator deaktiviert."
err = self._check_config() err = self._check_config(valves)
if err: if err:
return err return err
try: try:
api = self._api() api = self._api(valves)
await emitter.status(f"📄 Lade Seite {page_id}...") await emitter.status(f"📄 Lade Seite {page_id}...")
data = api._get(f"pages/{int(page_id)}") data = api._get(f"pages/{int(page_id)}")
@@ -420,31 +407,15 @@ class Tools:
lines.append(md if md else "*Kein Markdown-Inhalt verfügbar.*") lines.append(md if md else "*Kein Markdown-Inhalt verfügbar.*")
if fmt in ("html", "both"): if fmt in ("html", "both"):
lines.append("\n## Inhalt (HTML)") lines.append("\n## Inhalt (HTML)")
lines.append( lines.append(f"```html\n{html}\n```" if html else "*Kein HTML-Inhalt verfügbar.*")
f"```html\n{html}\n```" if html else "*Kein HTML-Inhalt verfügbar.*"
)
await emitter.status("✅ Seite geladen.", done=True) await emitter.status("✅ Seite geladen.", done=True)
return "\n".join(lines) return "\n".join(lines)
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
if e.response is not None
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
@@ -454,11 +425,12 @@ class Tools:
return msg return msg
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 3 Suche # TOOL 3 Suche
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def search_content( async def search_content(
self, self,
query: str, query: str,
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -468,24 +440,23 @@ class Tools:
:param query: The search string. :param query: The search string.
:return: A list of matching content items with IDs and types. :return: A list of matching content items with IDs and types.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_SEARCH: if not valves.ENABLE_SEARCH:
return "❌ Die Funktion 'Suche' ist vom Administrator deaktiviert." return "❌ Die Funktion 'Suche' ist vom Administrator deaktiviert."
err = self._check_config() err = self._check_config(valves)
if err: if err:
return err return err
try: try:
api = self._api() api = self._api(valves)
await emitter.status(f"🔍 Suche nach: '{query}'...") await emitter.status(f"🔍 Suche nach: '{query}'...")
data = api._get("search", params={"query": query, "count": 30}) data = api._get("search", params={"query": query, "count": 30})
results = data.get("data", []) results = data.get("data", [])
if not results: if not results:
await emitter.status( await emitter.status("✅ Suche abgeschlossen keine Ergebnisse.", done=True)
"✅ Suche abgeschlossen keine Ergebnisse.", done=True
)
return f"🔍 Keine Ergebnisse für **'{query}'**." return f"🔍 Keine Ergebnisse für **'{query}'**."
type_icons = { type_icons = {
@@ -508,7 +479,7 @@ class Tools:
preview_clean = re.sub(r"<[^>]+>", "", preview)[:200] preview_clean = re.sub(r"<[^>]+>", "", preview)[:200]
lines.append(f"{icon} **{name}** `[{itype.capitalize()} ID: {iid}]`") lines.append(f"{icon} **{name}** `[{itype.capitalize()} ID: {iid}]`")
if preview_clean: if preview_clean:
lines.append(f" > {preview_clean}") lines.append(f" > {preview_clean}")
lines.append("") lines.append("")
await emitter.status("✅ Suche abgeschlossen.", done=True) await emitter.status("✅ Suche abgeschlossen.", done=True)
@@ -516,22 +487,8 @@ class Tools:
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
if e.response is not None
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
@@ -541,7 +498,7 @@ class Tools:
return msg return msg
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 4 Seite erstellen # TOOL 4 Seite erstellen
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def create_page( async def create_page(
self, self,
@@ -550,6 +507,7 @@ class Tools:
content: str, content: str,
content_format: str = "markdown", content_format: str = "markdown",
chapter_id: Optional[int] = None, chapter_id: Optional[int] = None,
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -563,18 +521,17 @@ class Tools:
:param chapter_id: Optional chapter ID. Omit to place page at book level. :param chapter_id: Optional chapter ID. Omit to place page at book level.
:return: Confirmation with the new page's ID. :return: Confirmation with the new page's ID.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_CREATE_PAGE: if not valves.ENABLE_CREATE_PAGE:
return ( return "❌ Die Funktion 'Seite erstellen' ist vom Administrator deaktiviert."
"❌ Die Funktion 'Seite erstellen' ist vom Administrator deaktiviert." err = self._check_config(valves)
)
err = self._check_config()
if err: if err:
return err return err
try: try:
api = self._api() api = self._api(valves)
await emitter.status(f"✏️ Erstelle Seite '{title}'...") await emitter.status(f"✏️ Erstelle Seite '{title}'...")
payload: dict = {"book_id": int(book_id), "name": title} payload: dict = {"book_id": int(book_id), "name": title}
@@ -599,22 +556,8 @@ class Tools:
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
if e.response is not None
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
@@ -624,7 +567,7 @@ class Tools:
return msg return msg
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 5 Seite bearbeiten # TOOL 5 Seite bearbeiten
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def update_page( async def update_page(
self, self,
@@ -632,6 +575,7 @@ class Tools:
title: Optional[str] = None, title: Optional[str] = None,
content: Optional[str] = None, content: Optional[str] = None,
content_format: str = "markdown", content_format: str = "markdown",
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -644,13 +588,12 @@ class Tools:
:param content_format: Format of the content 'markdown' or 'html'. Default: 'markdown'. :param content_format: Format of the content 'markdown' or 'html'. Default: 'markdown'.
:return: Confirmation of the update. :return: Confirmation of the update.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_EDIT_PAGE: if not valves.ENABLE_EDIT_PAGE:
return ( return "❌ Die Funktion 'Seite bearbeiten' ist vom Administrator deaktiviert."
"❌ Die Funktion 'Seite bearbeiten' ist vom Administrator deaktiviert." err = self._check_config(valves)
)
err = self._check_config()
if err: if err:
return err return err
@@ -658,7 +601,7 @@ class Tools:
return "❌ Bitte mindestens 'title' oder 'content' angeben." return "❌ Bitte mindestens 'title' oder 'content' angeben."
try: try:
api = self._api() api = self._api(valves)
await emitter.status(f"📝 Aktualisiere Seite {page_id}...") await emitter.status(f"📝 Aktualisiere Seite {page_id}...")
current = api._get(f"pages/{int(page_id)}") current = api._get(f"pages/{int(page_id)}")
@@ -686,22 +629,8 @@ class Tools:
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
if e.response is not None
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
@@ -711,12 +640,13 @@ class Tools:
return msg return msg
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 6 Seite löschen # TOOL 6 Seite löschen
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def delete_page( async def delete_page(
self, self,
page_id: int, page_id: int,
confirm: bool = False, confirm: bool = False,
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -727,11 +657,12 @@ class Tools:
:param confirm: Must be True to confirm deletion. Default: False (safety guard). :param confirm: Must be True to confirm deletion. Default: False (safety guard).
:return: Confirmation or cancellation message. :return: Confirmation or cancellation message.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_DELETE_PAGE: if not valves.ENABLE_DELETE_PAGE:
return "❌ Die Funktion 'Seite löschen' ist vom Administrator deaktiviert." return "❌ Die Funktion 'Seite löschen' ist vom Administrator deaktiviert."
err = self._check_config() err = self._check_config(valves)
if err: if err:
return err return err
@@ -744,7 +675,7 @@ class Tools:
) )
try: try:
api = self._api() api = self._api(valves)
try: try:
page_info = api._get(f"pages/{int(page_id)}") page_info = api._get(f"pages/{int(page_id)}")
page_name = page_info.get("name", "Unbekannt") page_name = page_info.get("name", "Unbekannt")
@@ -759,22 +690,8 @@ class Tools:
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
if e.response is not None
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
@@ -784,12 +701,13 @@ class Tools:
return msg return msg
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 7 Buch erstellen # TOOL 7 Buch erstellen
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def create_book( async def create_book(
self, self,
name: str, name: str,
description: str = "", description: str = "",
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -799,16 +717,17 @@ class Tools:
:param description: Optional short description. :param description: Optional short description.
:return: Confirmation with the new book's ID. :return: Confirmation with the new book's ID.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_CREATE_BOOK: if not valves.ENABLE_CREATE_BOOK:
return "❌ Die Funktion 'Buch erstellen' ist vom Administrator deaktiviert." return "❌ Die Funktion 'Buch erstellen' ist vom Administrator deaktiviert."
err = self._check_config() err = self._check_config(valves)
if err: if err:
return err return err
try: try:
api = self._api() api = self._api(valves)
await emitter.status(f"📖 Erstelle Buch '{name}'...") await emitter.status(f"📖 Erstelle Buch '{name}'...")
result = api._post("books", {"name": name, "description": description}) result = api._post("books", {"name": name, "description": description})
@@ -822,22 +741,8 @@ class Tools:
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
if e.response is not None
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
@@ -847,13 +752,14 @@ class Tools:
return msg return msg
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 8 Buch bearbeiten # TOOL 8 Buch bearbeiten
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def update_book( async def update_book(
self, self,
book_id: int, book_id: int,
name: Optional[str] = None, name: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -865,13 +771,12 @@ class Tools:
:param description: Optional new description. :param description: Optional new description.
:return: Confirmation of the update. :return: Confirmation of the update.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_EDIT_BOOK: if not valves.ENABLE_EDIT_BOOK:
return ( return "❌ Die Funktion 'Buch bearbeiten' ist vom Administrator deaktiviert."
"❌ Die Funktion 'Buch bearbeiten' ist vom Administrator deaktiviert." err = self._check_config(valves)
)
err = self._check_config()
if err: if err:
return err return err
@@ -879,16 +784,12 @@ class Tools:
return "❌ Bitte mindestens 'name' oder 'description' angeben." return "❌ Bitte mindestens 'name' oder 'description' angeben."
try: try:
api = self._api() api = self._api(valves)
await emitter.status(f"📝 Aktualisiere Buch {book_id}...") await emitter.status(f"📝 Aktualisiere Buch {book_id}...")
current = api._get(f"books/{int(book_id)}") current = api._get(f"books/{int(book_id)}")
payload = { payload = {
"name": name if name is not None else current["name"], "name": name if name is not None else current["name"],
"description": ( "description": description if description is not None else current.get("description", ""),
description
if description is not None
else current.get("description", "")
),
} }
result = api._put(f"books/{book_id}", payload) result = api._put(f"books/{book_id}", payload)
@@ -902,22 +803,8 @@ class Tools:
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
if e.response is not None
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
@@ -927,13 +814,14 @@ class Tools:
return msg return msg
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 9 Kapitel erstellen # TOOL 9 Kapitel erstellen
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def create_chapter( async def create_chapter(
self, self,
book_id: int, book_id: int,
name: str, name: str,
description: str = "", description: str = "",
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -945,18 +833,17 @@ class Tools:
:param description: Optional short description. :param description: Optional short description.
:return: Confirmation with the new chapter's ID. :return: Confirmation with the new chapter's ID.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_CREATE_CHAPTER: if not valves.ENABLE_CREATE_CHAPTER:
return ( return "❌ Die Funktion 'Kapitel erstellen' ist vom Administrator deaktiviert."
"❌ Die Funktion 'Kapitel erstellen' ist vom Administrator deaktiviert." err = self._check_config(valves)
)
err = self._check_config()
if err: if err:
return err return err
try: try:
api = self._api() api = self._api(valves)
await emitter.status(f"📑 Erstelle Kapitel '{name}' in Buch {book_id}...") await emitter.status(f"📑 Erstelle Kapitel '{name}' in Buch {book_id}...")
result = api._post( result = api._post(
"chapters", "chapters",
@@ -973,22 +860,8 @@ class Tools:
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
if e.response is not None
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
@@ -998,13 +871,14 @@ class Tools:
return msg return msg
# ───────────────────────────────────────── # ─────────────────────────────────────────
# TOOL 10 Kapitel bearbeiten # TOOL 10 Kapitel bearbeiten
# ───────────────────────────────────────── # ─────────────────────────────────────────
async def update_chapter( async def update_chapter(
self, self,
chapter_id: int, chapter_id: int,
name: Optional[str] = None, name: Optional[str] = None,
description: Optional[str] = None, description: Optional[str] = None,
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None, __event_emitter__: Callable[[dict], Any] = None,
) -> str: ) -> str:
""" """
@@ -1016,11 +890,12 @@ class Tools:
:param description: Optional new description. :param description: Optional new description.
:return: Confirmation of the update. :return: Confirmation of the update.
""" """
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__) emitter = EventEmitter(__event_emitter__)
if not self.valves.ENABLE_EDIT_CHAPTER: if not valves.ENABLE_EDIT_CHAPTER:
return "❌ Die Funktion 'Kapitel bearbeiten' ist vom Administrator deaktiviert." return "❌ Die Funktion 'Kapitel bearbeiten' ist vom Administrator deaktiviert."
err = self._check_config() err = self._check_config(valves)
if err: if err:
return err return err
@@ -1028,17 +903,13 @@ class Tools:
return "❌ Bitte mindestens 'name' oder 'description' angeben." return "❌ Bitte mindestens 'name' oder 'description' angeben."
try: try:
api = self._api() api = self._api(valves)
await emitter.status(f"📝 Aktualisiere Kapitel {chapter_id}...") await emitter.status(f"📝 Aktualisiere Kapitel {chapter_id}...")
current = api._get(f"chapters/{int(chapter_id)}") current = api._get(f"chapters/{int(chapter_id)}")
payload = { payload = {
"book_id": current["book_id"], "book_id": current["book_id"],
"name": name if name is not None else current["name"], "name": name if name is not None else current["name"],
"description": ( "description": description if description is not None else current.get("description", ""),
description
if description is not None
else current.get("description", "")
),
} }
result = api._put(f"chapters/{chapter_id}", payload) result = api._put(f"chapters/{chapter_id}", payload)
@@ -1052,27 +923,12 @@ class Tools:
except requests.HTTPError as e: except requests.HTTPError as e:
body = e.response.text[:500] if e.response is not None else "(kein Body)" body = e.response.text[:500] if e.response is not None else "(kein Body)"
ct = ( ct = e.response.headers.get("Content-Type", "?") if e.response is not None else "?"
e.response.headers.get("Content-Type", "?") msg = f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\nContent-Type: {ct}\nAntwort: {body}"
if e.response is not None
else "?"
)
log.error(
"[BookStack] HTTP-Fehler %s | Content-Type=%s | Body=%s",
e.response.status_code,
ct,
body,
)
msg = (
f"❌ BookStack API-Fehler: HTTP {e.response.status_code}\n"
f"Content-Type: {ct}\n"
f"Antwort: {body}"
)
await emitter.error(msg) await emitter.error(msg)
return msg return msg
except Exception as e: except Exception as e:
log.exception("[BookStack] Unerwarteter Fehler: %s", e) log.exception("[BookStack] Unerwarteter Fehler: %s", e)
msg = f"❌ Unerwarteter Fehler: {type(e).__name__}: {e}" msg = f"❌ Unerwarteter Fehler: {type(e).__name__}: {e}"
await emitter.error(msg) await emitter.error(msg)
return msg return msg