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