ec81940178
- 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)
349 lines
10 KiB
Markdown
349 lines
10 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
permissions=[
|
|
"my_plugin:read",
|
|
"my_plugin:write",
|
|
"my_plugin:delete",
|
|
"my_plugin:admin",
|
|
],
|
|
```
|
|
|
|
### Routes absichern
|
|
|
|
Jede Route muss mit `require_permission` abgesichert werden:
|
|
|
|
```python
|
|
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()`:
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```sql
|
|
-- 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:
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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=[],
|
|
)
|
|
```
|
|
|
|
```python
|
|
# 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.*
|