Files
Bookstack-Connector/bookstack_connector.py

934 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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.4.0
license: MIT
requirements: requests
"""
import re
import logging
import requests
from typing import Any, Callable, Optional
from pydantic import BaseModel, Field
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger("BookStackConnector")
# ─────────────────────────────────────────────
# Helper: EventEmitter wrapper
# ─────────────────────────────────────────────
class EventEmitter:
def __init__(self, event_emitter: Callable[[dict], Any] = None):
self.event_emitter = event_emitter
async def status(self, description: str, done: bool = False):
if self.event_emitter:
await self.event_emitter(
{
"type": "status",
"data": {"description": description, "done": done},
}
)
async def error(self, description: str):
if self.event_emitter:
await self.event_emitter(
{
"type": "status",
"data": {"description": f"{description}", "done": True},
}
)
# ─────────────────────────────────────────────
# Helper: BookStack API client
# ─────────────────────────────────────────────
class BookStackAPI:
def __init__(self, base_url: str, token_id: str, token_secret: str):
self.base_url = base_url.rstrip("/")
self.headers = {
"Authorization": f"Token {token_id}:{token_secret}",
"Content-Type": "application/json",
}
def _parse_response(self, resp: requests.Response) -> dict:
log.debug(
"[BookStack] %s %s → HTTP %s | Content-Type: %s | Body[:300]: %s",
resp.request.method,
resp.url,
resp.status_code,
resp.headers.get("Content-Type", "?"),
resp.text[:300].replace("\n", " "),
)
resp.raise_for_status()
content_type = resp.headers.get("Content-Type", "")
if "application/json" not in content_type:
log.error(
"[BookStack] Antwort ist kein JSON! Content-Type=%s Body[:500]=%s",
content_type,
resp.text[:500],
)
raise ValueError(
f"BookStack antwortete nicht mit JSON.\n"
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/'"
)
return resp.json()
def _get(self, endpoint: str, params: dict = None) -> dict:
url = f"{self.base_url}/api/{endpoint}"
log.debug("[BookStack] GET %s params=%s", url, params)
resp = requests.get(url, headers=self.headers, params=params, timeout=15)
return self._parse_response(resp)
def _post(self, endpoint: str, data: dict) -> dict:
url = f"{self.base_url}/api/{endpoint}"
log.debug("[BookStack] POST %s payload=%s", url, data)
resp = requests.post(url, headers=self.headers, json=data, timeout=15)
return self._parse_response(resp)
def _put(self, endpoint: str, data: dict) -> dict:
url = f"{self.base_url}/api/{endpoint}"
log.debug("[BookStack] PUT %s payload=%s", url, data)
resp = requests.put(url, headers=self.headers, json=data, timeout=15)
return self._parse_response(resp)
def _delete(self, endpoint: str) -> bool:
url = f"{self.base_url}/api/{endpoint}"
log.debug("[BookStack] DELETE %s", url)
resp = requests.delete(url, headers=self.headers, timeout=15)
log.debug("[BookStack] DELETE → HTTP %s", resp.status_code)
resp.raise_for_status()
return True
def list_all(self, endpoint: str, count: int = 100) -> list:
"""Fetch all items with automatic pagination."""
items = []
offset = 0
while True:
data = self._get(endpoint, params={"count": count, "offset": offset})
batch = data.get("data", [])
items.extend(batch)
total = data.get("total", len(items))
offset += len(batch)
if offset >= total or not batch:
break
return items
# ─────────────────────────────────────────────
# Main Tool class
# ─────────────────────────────────────────────
class Tools:
class Valves(BaseModel):
BOOKSTACK_URL: str = Field(
default="",
description="Base URL deiner BookStack-Instanz, z.B. https://wiki.example.com",
)
API_TOKEN_ID: str = Field(
default="",
description="BookStack API Token ID (Profil → API Tokens)",
)
API_TOKEN_SECRET: str = Field(
default="",
description="BookStack API Token Secret",
)
MAX_ITEMS: int = Field(
default=100,
description="Maximale Anzahl Items pro API-Request (Pagination)",
)
ENABLE_LIST_STRUCTURE: bool = Field(
default=True,
description="✅ Erlauben: Gesamte Bibliotheksstruktur anzeigen (Shelves → Books → Chapters → Pages)",
)
ENABLE_READ_PAGE: bool = Field(
default=True,
description="✅ Erlauben: Seiteninhalte lesen (Markdown + HTML)",
)
ENABLE_SEARCH: bool = Field(
default=True,
description="✅ Erlauben: Inhalte durchsuchen",
)
ENABLE_CREATE_PAGE: bool = Field(
default=False,
description="⚠️ Erlauben: Neue Seiten erstellen",
)
ENABLE_EDIT_PAGE: bool = Field(
default=False,
description="⚠️ Erlauben: Bestehende Seiten bearbeiten",
)
ENABLE_DELETE_PAGE: bool = Field(
default=False,
description="🔴 Erlauben: Seiten löschen (UNWIDERRUFLICH!)",
)
ENABLE_CREATE_BOOK: bool = Field(
default=False,
description="⚠️ Erlauben: Neue Bücher erstellen",
)
ENABLE_EDIT_BOOK: bool = Field(
default=False,
description="⚠️ Erlauben: Bücher bearbeiten",
)
ENABLE_CREATE_CHAPTER: bool = Field(
default=False,
description="⚠️ Erlauben: Neue Kapitel erstellen",
)
ENABLE_EDIT_CHAPTER: bool = Field(
default=False,
description="⚠️ Erlauben: Kapitel bearbeiten",
)
def __init__(self):
self.valves = self.Valves()
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(
valves.BOOKSTACK_URL,
valves.API_TOKEN_ID,
valves.API_TOKEN_SECRET,
)
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'",
valves.BOOKSTACK_URL,
(tid[:4] + "***") if tid else "(leer)",
"***" if valves.API_TOKEN_SECRET else "(leer)",
)
if not valves.BOOKSTACK_URL:
return "❌ Bitte BOOKSTACK_URL in den Valve-Einstellungen eintragen."
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
# ─────────────────────────────────────────
async def get_library_structure(
self,
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None,
) -> str:
"""
Lists the complete BookStack library structure as a hierarchical overview.
Shows all Shelves, Books (with their Shelf assignment), Chapters and Pages
including their IDs. IDs are required for all read/edit/delete operations.
Use this as your starting point to navigate the wiki.
:return: A formatted Markdown tree of the entire library with IDs.
"""
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__)
if not valves.ENABLE_LIST_STRUCTURE:
return "❌ Die Funktion 'Bibliotheksstruktur anzeigen' ist vom Administrator deaktiviert."
err = self._check_config(valves)
if err:
return err
try:
api = self._api(valves)
await emitter.status("📚 Lade Shelves...")
shelves = api.list_all("shelves", valves.MAX_ITEMS)
await emitter.status("📖 Lade Bücher...")
books = api.list_all("books", valves.MAX_ITEMS)
await emitter.status("📑 Lade Kapitel...")
chapters = api.list_all("chapters", valves.MAX_ITEMS)
await emitter.status("📄 Lade Seiten...")
pages = api.list_all("pages", valves.MAX_ITEMS)
await emitter.status("🗂️ Baue Strukturbaum...")
shelf_book_map: dict = {s["id"]: [] for s in shelves}
for shelf in shelves:
detail = api._get(f"shelves/{shelf['id']}")
for b in detail.get("books", []):
shelf_book_map[shelf["id"]].append(b["id"])
book_map: dict = {b["id"]: b for b in books}
chapter_map: dict = {}
for ch in chapters:
chapter_map.setdefault(ch.get("book_id"), []).append(ch)
page_map_book: dict = {}
page_map_chapter: dict = {}
for p in pages:
if p.get("chapter_id"):
page_map_chapter.setdefault(p["chapter_id"], []).append(p)
else:
page_map_book.setdefault(p.get("book_id"), []).append(p)
shelved_book_ids = set(
bid for bids in shelf_book_map.values() for bid in bids
)
lines = []
lines.append("# 📚 BookStack Bibliotheksstruktur\n")
lines.append(
f"> **{len(shelves)} Regal(e)** · **{len(books)} Buch/Bücher** · "
f"**{len(chapters)} Kapitel** · **{len(pages)} Seite(n)**\n"
)
lines.append("---\n")
def render_book(book_id: int, indent: str = " "):
b = book_map.get(book_id)
if not b:
return
lines.append(f"{indent}📖 **{b['name']}** `[Book ID: {b['id']}]`")
for p in page_map_book.get(book_id, []):
lines.append(f"{indent} 📄 {p['name']} `[Page ID: {p['id']}]`")
for ch in chapter_map.get(book_id, []):
lines.append(
f"{indent} 📑 **{ch['name']}** `[Chapter ID: {ch['id']}]`"
)
for p in page_map_chapter.get(ch["id"], []):
lines.append(
f"{indent} 📄 {p['name']} `[Page ID: {p['id']}]`"
)
for shelf in shelves:
lines.append(
f"\n🗄️ **REGAL: {shelf['name']}** `[Shelf ID: {shelf['id']}]`"
)
if shelf.get("description"):
lines.append(f" *{shelf['description']}*")
book_ids = shelf_book_map.get(shelf["id"], [])
if not book_ids:
lines.append(" *(keine Bücher)*")
for bid in book_ids:
render_book(bid)
unshelved = [b for b in books if b["id"] not in shelved_book_ids]
if unshelved:
lines.append("\n📦 **BÜCHER OHNE REGAL**")
for b in unshelved:
render_book(b["id"])
lines.append("\n---")
lines.append(
"*Verwende die IDs für Lese-, Bearbeitungs- und Löschoperationen.*"
)
await emitter.status("✅ Struktur 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}\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
# ─────────────────────────────────────────
# 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:
"""
Reads the full content of a specific BookStack page by its ID.
Get page IDs from get_library_structure() or search_content().
:param page_id: The numeric ID of the page to read.
: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 valves.ENABLE_READ_PAGE:
return "❌ Die Funktion 'Seite lesen' ist vom Administrator deaktiviert."
err = self._check_config(valves)
if err:
return err
try:
api = self._api(valves)
await emitter.status(f"📄 Lade Seite {page_id}...")
data = api._get(f"pages/{int(page_id)}")
title = data.get("name", "Unbekannt")
md = data.get("markdown", "")
html = data.get("html", "")
updated = data.get("updated_at", "")
editor = data.get("editor", "")
lines = [
f"# 📄 {title}",
f"**Page ID:** `{page_id}` | **Book ID:** `{data.get('book_id')}` | "
f"**Chapter ID:** `{data.get('chapter_id') or ''}` | "
f"**Zuletzt geändert:** {updated} | **Editor:** {editor}",
"",
]
fmt = format.lower()
if fmt in ("markdown", "both"):
lines.append("## Inhalt (Markdown)")
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.*")
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 "?"
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
# ─────────────────────────────────────────
# TOOL 3 Suche
# ─────────────────────────────────────────
async def search_content(
self,
query: str,
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None,
) -> str:
"""
Searches across all BookStack content (pages, chapters, books, shelves).
Returns matching items with their IDs, types and a short preview.
: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 valves.ENABLE_SEARCH:
return "❌ Die Funktion 'Suche' ist vom Administrator deaktiviert."
err = self._check_config(valves)
if err:
return err
try:
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)
return f"🔍 Keine Ergebnisse für **'{query}'**."
type_icons = {
"page": "📄",
"chapter": "📑",
"book": "📖",
"bookshelf": "🗄️",
}
lines = [
f"# 🔍 Suchergebnisse für: '{query}'\n",
f"**{len(results)} Ergebnis(se):**\n",
]
for item in results:
icon = type_icons.get(item.get("type", ""), "")
name = item.get("name", "Unbekannt")
itype = item.get("type", "?")
iid = item.get("id", "?")
preview = item.get("preview_html", {}).get("content", "")
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("")
await emitter.status("✅ Suche abgeschlossen.", 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 "?"
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
# ─────────────────────────────────────────
# TOOL 4 Seite erstellen
# ─────────────────────────────────────────
async def create_page(
self,
book_id: int,
title: str,
content: str,
content_format: str = "markdown",
chapter_id: Optional[int] = None,
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None,
) -> str:
"""
Creates a new page in a BookStack book, optionally inside a chapter.
Get book_id and chapter_id from get_library_structure().
:param book_id: The ID of the target book.
:param title: Title of the new page.
:param content: The page content as a string.
:param content_format: Format of the content 'markdown' or 'html'. Default: 'markdown'.
: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 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(valves)
await emitter.status(f"✏️ Erstelle Seite '{title}'...")
payload: dict = {"book_id": int(book_id), "name": title}
if chapter_id:
payload["chapter_id"] = int(chapter_id)
if content_format.lower() == "html":
payload["html"] = content
else:
payload["markdown"] = content
result = api._post("pages", payload)
page_id = result.get("id")
await emitter.status("✅ Seite erstellt.", done=True)
return (
f"✅ **Seite erfolgreich erstellt!**\n"
f"- **Titel:** {title}\n"
f"- **Page ID:** `{page_id}`\n"
f"- **Book ID:** `{book_id}`\n"
f"- **Chapter ID:** `{chapter_id or ''}`"
)
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 "?"
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
# ─────────────────────────────────────────
# TOOL 5 Seite bearbeiten
# ─────────────────────────────────────────
async def update_page(
self,
page_id: int,
title: Optional[str] = None,
content: Optional[str] = None,
content_format: str = "markdown",
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None,
) -> str:
"""
Updates an existing page in BookStack. You can update the title, the content, or both.
Always fetch current content with get_page_content() first to avoid data loss.
:param page_id: The numeric ID of the page to update.
:param title: Optional new title. Omit to keep the current title.
:param content: Optional new content string. Omit to keep the current content.
: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 valves.ENABLE_EDIT_PAGE:
return "❌ Die Funktion 'Seite bearbeiten' ist vom Administrator deaktiviert."
err = self._check_config(valves)
if err:
return err
if title is None and content is None:
return "❌ Bitte mindestens 'title' oder 'content' angeben."
try:
api = self._api(valves)
await emitter.status(f"📝 Aktualisiere Seite {page_id}...")
current = api._get(f"pages/{int(page_id)}")
payload: dict = {
"book_id": int(current["book_id"]),
"name": title if title is not None else current["name"],
}
if content is not None:
if content_format.lower() == "html":
payload["html"] = content
else:
payload["markdown"] = content
else:
payload["markdown"] = current.get("markdown", "")
result = api._put(f"pages/{page_id}", payload)
await emitter.status("✅ Seite aktualisiert.", done=True)
return (
f"✅ **Seite erfolgreich aktualisiert!**\n"
f"- **Page ID:** `{page_id}`\n"
f"- **Neuer Titel:** {result.get('name')}\n"
f"- **Zuletzt geändert:** {result.get('updated_at')}"
)
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 "?"
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
# ─────────────────────────────────────────
# 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:
"""
Permanently deletes a page from BookStack. THIS ACTION IS IRREVERSIBLE.
ALWAYS ask the user to confirm before calling this. Only proceed with confirm=True.
:param page_id: The numeric ID of the page to delete.
: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 valves.ENABLE_DELETE_PAGE:
return "❌ Die Funktion 'Seite löschen' ist vom Administrator deaktiviert."
err = self._check_config(valves)
if err:
return err
if not confirm:
return (
f"⚠️ **Löschung nicht bestätigt.**\n"
f"Du bist dabei, **Page ID `{page_id}`** permanent zu löschen.\n"
"Diese Aktion **kann nicht rückgängig gemacht werden**. "
"Bitte bestätige mit `confirm=True`."
)
try:
api = self._api(valves)
try:
page_info = api._get(f"pages/{int(page_id)}")
page_name = page_info.get("name", "Unbekannt")
except Exception:
page_name = "Unbekannt"
await emitter.status(f"🗑️ Lösche Seite '{page_name}' ({page_id})...")
api._delete(f"pages/{int(page_id)}")
await emitter.status("✅ Seite gelöscht.", done=True)
return f"✅ **Seite gelöscht:** '{page_name}' (ID: `{page_id}`) wurde permanent entfernt."
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 "?"
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
# ─────────────────────────────────────────
# TOOL 7 Buch erstellen
# ─────────────────────────────────────────
async def create_book(
self,
name: str,
description: str = "",
__user__: dict = {},
__event_emitter__: Callable[[dict], Any] = None,
) -> str:
"""
Creates a new book in BookStack.
:param name: Name/title of the new book.
:param description: Optional short description.
:return: Confirmation with the new book's ID.
"""
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__)
if not valves.ENABLE_CREATE_BOOK:
return "❌ Die Funktion 'Buch erstellen' ist vom Administrator deaktiviert."
err = self._check_config(valves)
if err:
return err
try:
api = self._api(valves)
await emitter.status(f"📖 Erstelle Buch '{name}'...")
result = api._post("books", {"name": name, "description": description})
await emitter.status("✅ Buch erstellt.", done=True)
return (
f"✅ **Buch erfolgreich erstellt!**\n"
f"- **Name:** {name}\n"
f"- **Book ID:** `{result.get('id')}`\n"
f"- **Beschreibung:** {description or ''}"
)
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 "?"
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
# ─────────────────────────────────────────
# 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:
"""
Updates the name and/or description of an existing book.
Get book IDs from get_library_structure().
:param book_id: The numeric ID of the book to update.
:param name: Optional new name.
:param description: Optional new description.
:return: Confirmation of the update.
"""
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__)
if not valves.ENABLE_EDIT_BOOK:
return "❌ Die Funktion 'Buch bearbeiten' ist vom Administrator deaktiviert."
err = self._check_config(valves)
if err:
return err
if name is None and description is None:
return "❌ Bitte mindestens 'name' oder 'description' angeben."
try:
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", ""),
}
result = api._put(f"books/{book_id}", payload)
await emitter.status("✅ Buch aktualisiert.", done=True)
return (
f"✅ **Buch erfolgreich aktualisiert!**\n"
f"- **Book ID:** `{book_id}`\n"
f"- **Neuer Name:** {result.get('name')}\n"
f"- **Beschreibung:** {result.get('description') or ''}"
)
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 "?"
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
# ─────────────────────────────────────────
# 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:
"""
Creates a new chapter inside an existing book.
Get book IDs from get_library_structure().
:param book_id: The ID of the target book.
:param name: Name of the new chapter.
:param description: Optional short description.
:return: Confirmation with the new chapter's ID.
"""
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__)
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(valves)
await emitter.status(f"📑 Erstelle Kapitel '{name}' in Buch {book_id}...")
result = api._post(
"chapters",
{"book_id": int(book_id), "name": name, "description": description},
)
await emitter.status("✅ Kapitel erstellt.", done=True)
return (
f"✅ **Kapitel erfolgreich erstellt!**\n"
f"- **Name:** {name}\n"
f"- **Chapter ID:** `{result.get('id')}`\n"
f"- **Book ID:** `{book_id}`"
)
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 "?"
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
# ─────────────────────────────────────────
# 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:
"""
Updates the name and/or description of an existing chapter.
Get chapter IDs from get_library_structure().
:param chapter_id: The numeric ID of the chapter to update.
:param name: Optional new name.
:param description: Optional new description.
:return: Confirmation of the update.
"""
valves = self._get_valves(__user__)
emitter = EventEmitter(__event_emitter__)
if not valves.ENABLE_EDIT_CHAPTER:
return "❌ Die Funktion 'Kapitel bearbeiten' ist vom Administrator deaktiviert."
err = self._check_config(valves)
if err:
return err
if name is None and description is None:
return "❌ Bitte mindestens 'name' oder 'description' angeben."
try:
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", ""),
}
result = api._put(f"chapters/{chapter_id}", payload)
await emitter.status("✅ Kapitel aktualisiert.", done=True)
return (
f"✅ **Kapitel erfolgreich aktualisiert!**\n"
f"- **Chapter ID:** `{chapter_id}`\n"
f"- **Neuer Name:** {result.get('name')}\n"
f"- **Beschreibung:** {result.get('description') or ''}"
)
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 "?"
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