- 0.7: UI-Design-Richtlinien (docs/ui-design-guidelines.md, 535 lines) - 0.8: Theme-Customization Backend (4 theme fields, migration 0023) - 0.9: Theme-Customization Frontend (SettingsTheme.tsx, themeStore.ts, live preview) - 0.10: RBAC-Audit (4 plugins secured, 53 routes with require_permission) - 0.11: LiteLLM-Cleanup (llm_client.py migrated from httpx to litellm) - 0.12: KI-Agent-Framework docs (plugin-development-guide.md, agent_capabilities field) - 0.13: Heartbeat configurable (ProactiveSettings, migration 0024, frontend UI) - 0.14: Unified Search Field-Level RBAC (resolve_permissions + filter_fields_by_permission) - 0.15: Undo/History-System (EntityHistory model, service, routes, migration 0025, HistoryViewer) - 0.16: Storage Backend (LocalStorage + S3Storage, DMS/attachments/mail updated) - 0.17: Import/Export unified Contact fields (firstname, surname, email_1, phone_1) - 0.18: .gitignore & Config-Cleanup (webui→frontend, python-jose removed, .env untracked) - 0.19: Mail-Salt Security-Fix (per-account random salt, migration 0026) - 0.20: AGPL replaced (PyMuPDF→pypdf, OnlyOffice→Collabora, LICENSE + THIRD_PARTY_LICENSES.md)
10 KiB
LeoCRM Plugin Development Guide
Version: 1.0
Datum: 2026-07-23
Gültig für: Alle Plugin-Entwickler
1. Plugin-Struktur
Jedes Plugin liegt unter app/plugins/builtins/<plugin_name>/:
app/plugins/builtins/my_plugin/
├── __init__.py
├── plugin.py # Plugin-Klasse mit Manifest
├── routes.py # API-Routes
├── models.py # SQLAlchemy-Modelle (optional)
├── schemas.py # Pydantic-Schemas (optional)
├── services.py # Business-Logik (optional)
├── migrations/ # SQL-Migrationen
│ └── 0001_initial.sql
└── tests/ # Plugin-Tests (optional)
2. Plugin-Manifest
Das Manifest definiert Metadaten, Abhängigkeiten, Routes, Events und Permissions:
from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest, PluginRouteDef
class MyPlugin(BasePlugin):
manifest = PluginManifest(
name="my_plugin",
version="1.0.0",
display_name="My Plugin",
description="Description of what the plugin does.",
dependencies=["permissions"], # Other plugins this depends on
routes=[
PluginRouteDef(
path="/api/v1/my-plugin",
module="app.plugins.builtins.my_plugin.routes",
router_attr="router",
),
],
events=["contact.created", "contact.updated"],
migrations=["0001_initial.sql"],
permissions=[
"my_plugin:read",
"my_plugin:write",
"my_plugin:delete",
],
agent_capabilities=[
"my_plugin:search",
"my_plugin:analyze",
],
)
Manifest-Felder
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
name |
str |
Ja | Eindeutiger Plugin-Name (snake_case) |
version |
str |
Ja | Semantic Version |
display_name |
str |
Ja | Anzeigename |
description |
str |
Nein | Kurzbeschreibung |
dependencies |
list[str] |
Nein | Andere Plugins, die geladen sein müssen |
routes |
list[PluginRouteDef] |
Nein | API-Routen-Definitionen |
events |
list[str] |
Nein | Events, die das Plugin abonniert |
migrations |
list[str] |
Nein | SQL-Migrationsdateien |
permissions |
list[str] |
Nein | RBAC-Permissions, die das Plugin definiert |
field_definitions |
list[FieldDefinition] |
Nein | Feld-Level-Permissions |
agent_capabilities |
list[str] |
Nein | KI-Agent-Fähigkeiten, die das Plugin bietet |
is_core |
bool |
Nein | Core-Plugin (kann nicht deaktiviert werden) |
3. RBAC-Permissions
Permissions definieren
Im Manifest werden alle Permissions des Plugins aufgelistet:
permissions=[
"my_plugin:read",
"my_plugin:write",
"my_plugin:delete",
"my_plugin:admin",
],
Routes absichern
Jede Route muss mit require_permission abgesichert werden:
from app.deps import get_current_user, require_permission
from fastapi import Depends
@router.get("", dependencies=[Depends(require_permission("my_plugin:read"))])
async def list_items(current_user: dict = Depends(get_current_user)):
...
@router.post("", status_code=201, dependencies=[Depends(require_permission("my_plugin:write"))])
async def create_item(data: ItemCreate, current_user: dict = Depends(get_current_user)):
...
@router.delete("/{item_id}", dependencies=[Depends(require_permission("my_plugin:delete"))])
async def delete_item(item_id: str, current_user: dict = Depends(get_current_user)):
...
Permission-Namenskonvention
- Format:
<plugin_name>:<action> - Standard-Actions:
read,write,delete,share,admin - Beispiele:
calendar:read,dms:write,tags:delete
4. KI-Agent-Framework
LeoCRM bietet ein integriertes KI-Agent-Framework basierend auf LiteLLM und PydanticAI.
Architektur
Plugin (ai_assistant, ai_proactive, zukünftige)
↓
LiteLLM (unified LLM interface — 100+ Provider)
↓
Provider (OpenAI, Anthropic, Google, Ollama, ...)
↑
Tool Registry (Plugin-Tools für KI-Agenten)
LiteLLM — Unified LLM Interface
LiteLLM bietet eine einheitliche API für über 100 LLM-Provider. Alle KI-Funktionen
in LeoCRM nutzen litellm.acompletion():
import litellm
response = await litellm.acompletion(
model="openai/gpt-4o", # oder anthropic/claude-3-sonnet, ollama/llama3
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query},
],
temperature=0.3,
max_tokens=1000,
api_key=os.environ.get("AI_API_KEY"),
api_base=os.environ.get("AI_API_BASE"), # optional für self-hosted
)
content = response.choices[0].message.content
Konfiguration
| Env-Var | Beschreibung | Standard |
|---|---|---|
AI_MODEL |
Modell-Name (z.B. gpt-4o, claude-3-sonnet, llama3) |
— |
AI_API_KEY |
API-Key für den Provider | — |
AI_API_BASE |
Custom API-Base-URL (optional) | Provider-Standard |
AI_PROVIDER |
Provider-Präfix (openai, anthropic, google, ollama) |
openai |
Wenn AI_MODEL und AI_API_KEY nicht gesetzt sind, läuft der LLM-Client im Mock-Modus
(keyword-basierte Action-Mapping für Tests).
Tool Registry — KI-Tools registrieren
Plugins können Tools registrieren, die KI-Agenten während Chat-Sessions aufrufen können. Jedes Tool deklariert Name, Beschreibung, JSON-Schema für Parameter und einen async Handler.
from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry
registry = get_tool_registry()
registry.register(
name="search_contacts",
description="Search contacts by name, email, or phone number",
parameters={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"limit": {"type": "integer", "description": "Max results", "default": 10},
},
"required": ["query"],
},
handler=my_search_handler,
plugin_name="my_plugin",
required_permission="contacts:read",
category="search",
)
Tool Handler
Der Handler ist eine async Funktion, die Argumente und Kontext empfängt:
async def my_search_handler(arguments: dict, context: dict) -> str:
query = arguments.get("query", "")
limit = arguments.get("limit", 10)
# ... perform search ...
return json.dumps({"results": results})
Tools bei Plugin-Deaktivierung abmelden
def on_deactivate(self):
registry = get_tool_registry()
registry.unregister_plugin("my_plugin")
Agent Capabilities im Manifest
Das agent_capabilities Feld im Manifest deklariert, welche KI-Fähigkeiten ein Plugin bietet:
agent_capabilities=[
"contact_search", # Kontakt-Suche
"email_draft", # E-Mail-Entwürfe generieren
"calendar_scheduling", # Terminvorschläge
],
Diese Informationen werden vom AI Assistant verwendet, um Nutzern zu zeigen, welche KI-Funktionen verfügbar sind.
5. Events
Plugins können Events abonnieren und auslösen:
# Im Manifest:
events=["contact.created", "contact.updated", "contact.deleted"]
# Event-Handler im Plugin:
async def on_contact_created(self, event_data: dict):
# Reagiere auf neues Kontakt-Event
pass
Events werden vom Event-Publisher im Contact-Service ausgelöst:
from app.core.events import publish_event
await publish_event(db, "contact.created", {"contact_id": str(contact.id)})
6. Datenbank-Migrationen
SQL-Migrationen liegen unter migrations/ im Plugin-Verzeichnis:
-- migrations/0001_initial.sql
CREATE TABLE my_plugin_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
name VARCHAR(200) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
Im Manifest referenzieren:
migrations=["0001_initial.sql"]
7. UI-Integration
Siehe docs/ui-design-guidelines.md für Frontend-Konventionen.
- Plugin-Seiten verwenden das 3-Spalten-Explorer-Layout
- PluginToolbar für Aktionen
- Plugin-Settings als eigene Settings-Sub-Seite
- i18n-Keys mit Plugin-Präfix
8. Testing
Tests liegen unter tests/ im Plugin-Verzeichnis oder im zentralen tests/ Ordner:
# tests/test_my_plugin.py
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_list_items_requires_permission(client: AsyncClient, auth_headers):
response = await client.get("/api/v1/my-plugin", headers=auth_headers)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_list_items_without_permission_returns_403(client: AsyncClient, no_perm_headers):
response = await client.get("/api/v1/my-plugin", headers=no_perm_headers)
assert response.status_code == 403
9. Plugin-Beispiel
Minimal-Beispiel für ein neues Plugin:
# app/plugins/builtins/my_plugin/plugin.py
from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest, PluginRouteDef
class MyPlugin(BasePlugin):
manifest = PluginManifest(
name="my_plugin",
version="1.0.0",
display_name="My Plugin",
description="A minimal example plugin.",
dependencies=[],
routes=[
PluginRouteDef(
path="/api/v1/my-plugin",
module="app.plugins.builtins.my_plugin.routes",
router_attr="router",
),
],
events=[],
migrations=[],
permissions=["my_plugin:read", "my_plugin:write"],
agent_capabilities=[],
)
# app/plugins/builtins/my_plugin/routes.py
from fastapi import APIRouter, Depends
from app.deps import get_current_user, require_permission
router = APIRouter()
@router.get("", dependencies=[Depends(require_permission("my_plugin:read"))])
async def list_items(current_user: dict = Depends(get_current_user)):
return {"items": []}
Dieses Dokument ist verbindlich für alle Plugin-Entwicklung an LeoCRM.