Files
leocrm/docs/bauplan-unified-messaging.md
T
Agent Zero cc3ac9a43d feat: Unified Messaging System — kommunikation plugin, AI/Proactive/System participants, MessageSidebar, Rich Content Renderer
Phase 1: Backend plugin kommunikation (13 files, 10 tables, REST API, WebSocket, RBAC, DMS Bridge, Participant Registry, Mini-App Registry, Search Provider)
Phase 2: AI plugins as participants (ai_assistant + ai_proactive dock as participants, heartbeat job)
Phase 3: system_notif plugin (system events → chat messages, pinned System room)
Phase 4: Frontend MessageSidebar (replaces AISidebar, same design, comm API client, WebSocket hook, commStore)
Phase 5: Rich Content Block Renderer (11 components: Markdown, HTML, Image, Audio, Video, File, ActionCard, ContactCard, MiniApp, BlockRenderer)
BasePlugin: added services property + _container in on_activate
2026-07-22 01:22:15 +02:00

1634 lines
66 KiB
Markdown
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.
# Bauplan: Unified Messaging System für LeoCRM
> Vollständiger Bauplan unter Berücksichtigung aller bestehenden Systeme,
> Plugin-Architektur, RBAC, DMS, Suche und aller Nutzer-Anforderungen.
---
## 1. Bestandsaufnahme — Was existiert bereits
### 1.1 Plugin-System
- **BasePlugin** mit Lifecycle Hooks: `on_install`, `on_activate`, `on_deactivate`, `on_uninstall`
- **PluginManifest** mit: name, version, dependencies, routes, events, migrations, permissions, is_core
- **PluginRegistry** mit: Discovery, Install, Activate, Deactivate, Uninstall, Topological Sort, Dependency Resolution
- **EventBus** — in-process pub/sub, async handlers
- **MigrationRunner** — SQL-Migrations pro Plugin
- **ServiceContainer** — shared services
### 1.2 Bestehende Plugins
| Plugin | Status | Abhängigkeiten | is_core | Eigene UI | Eigene Tabellen |
|---|---|---|---|---|---|
| `ai_assistant` | Aktiv | — | ✅ | Ja (ChatWindow, Sessions) | ai_chat_sessions, ai_chat_messages, ai_chat_attachments, ai_chat_folders, ai_providers, ai_models, ai_presets, ai_agents |
| `ai_proactive` | Aktiv | ai_assistant, unified_search | ❌ | Ja (SuggestionList, SSE) | ai_proactive_suggestions, ai_proactive_context_log, ai_proactive_settings |
| `dms` | Aktiv | permissions | ❌ | Ja (DMS Page) | folders, files |
| `mail` | Aktiv | — | ❌ | Ja (Mail Page) | mails |
| `calendar` | Aktiv | — | ❌ | Ja (Calendar Page) | calendar_events |
| `unified_search` | Aktiv | — | ❌ | Ja (Search Page) | search_index |
| `permissions` | Aktiv | — | ❌ | Ja (Settings) | permissions, role_permissions |
| `report_generator` | Aktiv | — | ❌ | Ja | — |
### 1.3 Notification-System (Core, nicht Plugin)
- **Modelle:** `Notification`, `NotificationType`, `NotificationPreference`
- **Core Service:** `app/core/notifications.py``create_notification()`
- **Routes:** `app/routes/notifications.py`
- **Plugin Integration:** Plugins deklarieren Notification-Types via `get_notification_types()`
- **Sync:** `PluginRegistry.sync_notification_types()` → DB
- **Frontend:** Aktuell im uiStore als `string[]` (Mock-Daten)
### 1.4 RBAC / Permission System
- **Permission Registry:** `app/core/permission_registry.py`
- **Permission Check:** `app/core/permissions.py``check_permission()`, `get_cached_permissions()`
- **FastAPI Deps:** `require_permission()`, `require_admin()`, `require_write()`
- **Modelle:** `Permission`, `Role`, `RolePermission` (permissions plugin)
- **User Session:** `get_current_user` → liefert `permissions[]`, `denied_permissions[]`, `field_permissions{}`, `is_system_admin`
- **Pattern:** `module:action` (z.B. `comm:read`, `comm:write`, `comm:manage`)
### 1.5 DMS Plugin
- **Modelle:** `Folder` (hierarchisch, tenant-scoped, soft-deletable), `File` (storage_path auf Disk, mime_type, size_bytes)
- **Storage:** `/tmp/dms` (configurable via `DMS_STORAGE_BASE`)
- **Sharing:** Internal sharing via permissions
- **OnlyOffice:** Edit sessions für Office-Dateien
- **Upload:** `UploadFile` → Disk + DB-Eintrag
### 1.6 AI Assistant Plugin (Detail)
- **Provider-Verwaltung:** Multi-Provider (OpenAI, Ollama, etc.), API-Keys in DB
- **Modelle/Preset/Agents:** Konfigurierbar pro Tenant
- **Chat Sessions:** Eigene Tabellen, Streaming via SSE
- **Tool Registry:** Plugins können AI-Tools registrieren (`tool_registry.py`)
- **Frontend:** `ChatWindow` Komponente, `AIAssistant` Page, `AISettings` Page
### 1.7 AI Proactive Plugin (Detail)
- **Suggestion Engine:** Kontext-Änderung → LLM → Suggestion
- **SSE Push:** `_sse_queues` dict, `push_suggestion()`
- **Background Jobs:** `deep_analysis` via ARQ
- **Settings:** Pro-User (enabled, categories, confidence_threshold, rate_limit, model)
- **Frontend:** `SuggestionList` Komponente, `ProactiveAISettings` Page
- **Events:** `context.view_changed`, `context.entity_selected`
### 1.8 Frontend Sidebar Struktur
- **Linke Sidebar:** Navigation (Dashboard, Kontakte, Kalender, Dateien, Mail)
- **Rechte Sidebar (AISidebar):** 5 Tabs — KI Chat, Live KI, Benachrichtigungen, Team, Chat
- **uiStore:** `aiSidebarCollapsed`, `aiSidebarTab`, `notifications: string[]`
- **Komponenten:** `ChatWindow`, `SuggestionList`, `TeamPanel`, `ChatPanel` (placeholder)
### 1.9 AI Copilot System (Core, nicht Plugin)
- **Modelle:** `AIConversation`, `AIMessage` (tenant-scoped)
- **Service:** `ai_copilot_service` — NL → proposed actions → execute
- **Routes:** `/api/v1/ai/copilot/query`, `/api/v1/ai/copilot/execute`
- **Frontend:** AIAssistant Page nutzt Copilot
---
## 2. Architektur — Plugin-Ebenen
### Übersicht
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend │
│ ┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐ │
│ │ Linke Sidebar │ │ Hauptbereich │ │ Rechte Sidebar │ │
│ │ Navigation │ │ (CRM Seiten) │ │ = MessageSidebar │ │
│ │ │ │ │ │ Raum-Liste + Feed │ │
│ │ │ │ │ │ + Eingabefeld │ │
│ └──────────────┘ └───────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │ │
│ │ │ WebSocket
┌────────┴────────────────────┴────────────────────────┴──────────┐
│ Plugin: kommunikation (Core) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ Conversations│ │ Messages │ │ Participant Registry │ │
│ │ + Räume │ │ + Blocks │ │ (Andockpunkt) │ │
│ │ + Locking │ │ + Attachments│ │ │ │
│ │ + RBAC │ │ + Reactions │ │ register(type, handler) │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ WebSocket Mgr│ │ DMS Bridge │ │ Mini-App Registry │ │
│ │ (Real-time) │ │ (File Store) │ │ (Plugin Blocks) │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────────┘
│ EventBus + Participant Registry
┌──────────────────────────┴──────────────────────────────────────┐
│ Plugin: ai_assistant │ Plugin: ai_proactive │ Plugin: │
│ (Teilnehmer: ai) │ (Teilnehmer: ai_p) │ system_notif │
│ @KI → Response │ Heartbeat → Kanal │ (Teilnehmer: │
│ Keine eigene UI │ Keine eigene UI │ system) │
│ Nutzt comm UI │ Nutzt comm UI │ Events→Msg │
└─────────────────────────────────────────────────────────────────┘
┌──────────────────────────┴──────────────────────────────────────┐
│ Später: whatsapp_gateway │ telegram_gateway │ email_gateway │
│ (Teilnehmer: whatsapp) │ (Teilnehmer: telegram) │ (Teilnehmer: email)│
└─────────────────────────────────────────────────────────────────┘
```
---
## 3. Plugin `kommunikation` (Core-Plugin)
### 3.1 Manifest
```python
PluginManifest(
name="kommunikation",
version="1.0.0",
display_name="Kommunikation",
description="Unified Messaging: Chat, KI, System, Messenger — alles ist ein Teilnehmer",
dependencies=["permissions", "dms"],
routes=[
PluginRouteDef(path="/api/v1/comm", module="...routes", router_attr="router"),
],
events=[
"message.received",
"message.sent",
"conversation.created",
"conversation.updated",
"participant.joined",
"participant.left",
"reaction.added",
],
migrations=["0001_initial.sql"],
permissions=[
"comm:read", # Nachrichten/Konversationen lesen
"comm:write", # Nachrichten schreiben
"comm:create", # Konversationen erstellen
"comm:manage", # Teilnehmer verwalten, Räume locken
"comm:admin", # Konversation-Admin (Rollen vergeben)
"comm:delete", # Nachrichten/Konversationen löschen
],
is_core=True,
)
```
### 3.2 Komponenten
```
app/plugins/builtins/kommunikation/
├── __init__.py
├── plugin.py # Plugin-Klasse, Lifecycle
├── manifest.py # (in plugin.py)
├── models.py # SQLAlchemy-Modelle
├── schemas.py # Pydantic-Schemas
├── routes.py # REST-API + WebSocket
├── services.py # Business Logic
├── participant_registry.py # Teilnehmer-Interface + Registry
├── content_types.py # Block-Typ-Definitionen
├── miniapp_registry.py # Mini-App Registry
├── websocket_manager.py # WebSocket-Verbindungs-Manager
├── dms_bridge.py # DMS-Integration für Attachments
├── rbac.py # Chat-interne RBAC-Logik
├── search_provider.py # unified_search Provider
└── migrations/
└── 0001_initial.sql
```
### 3.3 Datenmodell
#### Tabelle `comm_conversations`
```sql
CREATE TABLE comm_conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
title TEXT, -- Raum-Name (NULL = Direkt-Chat)
title_set_by UUID, -- user_id die den Titel gesetzt hat
is_pinned BOOLEAN DEFAULT FALSE, -- von User angepinnt
is_locked BOOLEAN DEFAULT FALSE, -- Plugin-Lock (User kann nicht ändern)
locked_by TEXT, -- Plugin-Name der gelockt hat
is_direct BOOLEAN DEFAULT FALSE, -- 1:1 vs Gruppe/Raum
is_archived BOOLEAN DEFAULT FALSE, -- archiviert
created_by UUID, -- user_id oder plugin_name
created_by_type TEXT DEFAULT 'user', -- 'user', 'plugin', 'system'
last_msg_at TIMESTAMPTZ,
last_msg_preview TEXT, -- für Listen-Anzeige
last_msg_sender_type TEXT, -- für Icon in Liste
metadata JSONB DEFAULT '{}', -- z.B. {"pinned_by": "user_id"}
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ix_comm_conversations_tenant ON comm_conversations(tenant_id);
CREATE INDEX ix_comm_conversations_tenant_pinned ON comm_conversations(tenant_id, is_pinned);
CREATE INDEX ix_comm_conversations_last_msg ON comm_conversations(tenant_id, last_msg_at DESC);
```
#### Tabelle `comm_participants`
```sql
CREATE TABLE comm_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
conversation_id UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
participant_id UUID, -- user_id (NULL für ai/system/gateways)
participant_type TEXT NOT NULL, -- 'user', 'ai', 'ai_proactive', 'system', 'whatsapp', ...
display_name TEXT, -- Override-Name
role TEXT DEFAULT 'member', -- 'admin', 'member', 'reader'
joined_at TIMESTAMPTZ DEFAULT NOW(),
left_at TIMESTAMPTZ,
UNIQUE(conversation_id, participant_id, participant_type)
);
CREATE INDEX ix_comm_participants_tenant_conv ON comm_participants(tenant_id, conversation_id);
CREATE INDEX ix_comm_participants_tenant_user ON comm_participants(tenant_id, participant_id);
```
#### Tabelle `comm_messages`
```sql
CREATE TABLE comm_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
conversation_id UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
sender_id UUID, -- user_id (NULL für ai/system)
sender_type TEXT NOT NULL, -- 'user', 'ai', 'ai_proactive', 'system', ...
content TEXT NOT NULL DEFAULT '', -- Text-Inhalt (Fallback)
content_format TEXT DEFAULT 'text', -- 'text', 'markdown', 'html'
metadata JSONB DEFAULT '{}', -- typ-spezifische Daten
reply_to_id UUID REFERENCES comm_messages(id) ON DELETE SET NULL,
is_pinned BOOLEAN DEFAULT FALSE, -- Nachricht angepinnt im Feed
created_at TIMESTAMPTZ DEFAULT NOW(),
read_at TIMESTAMPTZ, -- veraltet → comm_message_reads
edited_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ
);
CREATE INDEX ix_comm_messages_tenant_conv ON comm_messages(tenant_id, conversation_id, created_at);
CREATE INDEX ix_comm_messages_tenant_sender ON comm_messages(tenant_id, sender_id);
```
#### Tabelle `comm_message_blocks` (Rich Content)
```sql
CREATE TABLE comm_message_blocks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
message_id UUID NOT NULL REFERENCES comm_messages(id) ON DELETE CASCADE,
block_type TEXT NOT NULL, -- 'text', 'markdown', 'html', 'image', 'audio', 'video', 'file', 'action_card', 'contact_card', 'miniapp', ...
block_data JSONB NOT NULL, -- typ-spezifische strukturierte Daten
sort_order INT DEFAULT 0
);
CREATE INDEX ix_comm_blocks_tenant_msg ON comm_message_blocks(tenant_id, message_id);
```
#### Tabelle `comm_message_attachments` (DMS-Referenzen)
```sql
CREATE TABLE comm_message_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
message_id UUID NOT NULL REFERENCES comm_messages(id) ON DELETE CASCADE,
file_id UUID, -- DMS file_id (Referenz)
file_source TEXT DEFAULT 'comm', -- 'comm' (eigener DMS-Bereich) oder 'dms' (externer Verweis)
file_name TEXT NOT NULL,
file_type TEXT NOT NULL, -- MIME type
file_size BIGINT,
thumbnail_path TEXT, -- für Bilder/Videos
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ix_comm_attachments_tenant_msg ON comm_message_attachments(tenant_id, message_id);
```
#### Tabelle `comm_message_reactions`
```sql
CREATE TABLE comm_message_reactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
message_id UUID NOT NULL REFERENCES comm_messages(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
emoji TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(message_id, user_id, emoji)
);
CREATE INDEX ix_comm_reactions_tenant_msg ON comm_message_reactions(tenant_id, message_id);
```
#### Tabelle `comm_message_reads` (Lese-Status pro User pro Konversation)
```sql
CREATE TABLE comm_message_reads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
conversation_id UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
last_read_msg_id UUID REFERENCES comm_messages(id) ON DELETE SET NULL,
last_read_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(conversation_id, user_id)
);
CREATE INDEX ix_comm_reads_tenant_user ON comm_message_reads(tenant_id, user_id);
```
#### Tabelle `comm_conversation_pins` (User-spezifisches Pinning)
```sql
CREATE TABLE comm_conversation_pins (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
conversation_id UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
pinned_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(conversation_id, user_id)
);
CREATE INDEX ix_comm_pins_tenant_user ON comm_conversation_pins(tenant_id, user_id);
```
#### Tabelle `comm_message_edits` (Bearbeitung-Historie)
```sql
CREATE TABLE comm_message_edits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
message_id UUID NOT NULL REFERENCES comm_messages(id) ON DELETE CASCADE,
old_content TEXT NOT NULL,
old_blocks JSONB DEFAULT '[]',
edited_by UUID NOT NULL,
edited_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ix_comm_edits_tenant_msg ON comm_message_edits(tenant_id, message_id);
```
#### Tabelle `comm_conversation_mutes` (Stummschaltung pro User)
```sql
CREATE TABLE comm_conversation_mutes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
conversation_id UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
muted_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(conversation_id, user_id)
);
CREATE INDEX ix_comm_mutes_tenant_user ON comm_conversation_mutes(tenant_id, user_id);
```
### 3.4 Raum-Logik
**Räume = Benannte Konversationen mit Pinning und Locking**
| Eigenschaft | Wer setzt es | Bedeutung |
|---|---|---|
| `title` | User oder Plugin | Raum-Name. NULL = Direkt-Chat (auto-Titel aus Teilnehmern) |
| `is_pinned` (Tabelle `comm_conversation_pins`) | User | User-spezifisches Anpinnen für Sortierung |
| `is_locked` | Plugin | Plugin-Lock: User kann Titel/Teilnehmer nicht ändern |
| `locked_by` | Plugin | Plugin-Name der gelockt hat |
| `is_direct` | System | TRUE = 1:1 Chat, FALSE = Gruppe/Raum |
| `is_archived` | User | Archiviert → erscheint nicht in Standard-Liste |
**Plugin-erstellte Räume:**
- Plugin erstellt Konversation via `services.create_conversation()`
- Setzt `is_locked=True`, `locked_by='plugin_name'`, `created_by_type='plugin'`
- Fügt sich selbst als Participant hinzu (`participant_type='system'` etc.)
- Fügt User als Participant hinzu (`role='reader'` oder `role='member'`)
- Pinnt den Raum für den User (`comm_conversation_pins`)
**Beispiele für Plugin-Räume:**
- `system_notif` → Raum "System" (locked, gepinnt) — System-Notifications
- `ai_proactive` → Raum "Live KI" (locked, gepinnt) — Proaktive Vorschläge + Heartbeat
- `ai_assistant` → Raum "Assistent" (locked, gepinnt) — 1:1 KI-Chat
### 3.5 Participant Registry
```python
class ParticipantHandler(ABC):
"""Interface das Plugins implementieren um als Teilnehmer zu fungieren."""
@abstractmethod
async def on_message_received(
self,
conversation_id: uuid.UUID,
message: dict, # Die neue Nachricht
conversation: dict, # Die gesamte Konversation mit Teilnehmern
mentions: list[str], # Geparste @Mentions (Teilnehmer-Typen)
context: dict # Tenant, user, etc.
) -> list[dict] | None:
"""Wird aufgerufen wenn eine neue Nachricht in einer Konversation
ankommt, an der dieser Teilnehmer beteiligt ist.
Rückgabe: Optional Liste von neuen Nachrichten (z.B. AI-Response).
Für reine Leser (system) → return None.
Für reaktive Teilnehmer (ai) → return [message_dict, ...].
"""
pass
@abstractmethod
def get_participant_info(self) -> dict:
"""Metadaten: display_name, avatar_url, capabilities, description."""
pass
class ParticipantRegistry:
"""Global registry für Plugin-Teilnehmer."""
def __init__(self):
self._handlers: dict[str, ParticipantHandler] = {}
def register(self, participant_type: str, handler: ParticipantHandler) -> None:
self._handlers[participant_type] = handler
def unregister(self, participant_type: str) -> None:
self._handlers.pop(participant_type, None)
def get_handler(self, participant_type: str) -> ParticipantHandler | None:
return self._handlers.get(participant_type)
def list_types(self) -> list[str]:
return list(self._handlers.keys())
# Global instance
_registry = ParticipantRegistry()
def get_participant_registry() -> ParticipantRegistry:
return _registry
```
### 3.6 DMS Bridge
Das `kommunikation` Plugin nutzt das DMS für Datei-Speicherung:
```python
class DmsBridge:
"""Bridge zum DMS Plugin für Attachment-Speicherung."""
COMM_FOLDER_NAME = "_kommunikation" # Versteckter Root-Ordner
async def ensure_comm_folder(self, db, tenant_id) -> Folder:
"""Stellt sicher dass der Kommunikation-Ordner im DMS existiert."""
# Prüfe ob Folder existiert
# Wenn nicht: Erstelle Root-Folder '_kommunikation'
# Pro Konversation: Sub-Ordner mit Konversations-ID
async def store_attachment(
self, db, tenant_id, conversation_id, user_id, file: UploadFile
) -> dict:
"""Speichert eine Datei im DMS unter _kommunikation/{conversation_id}/."""
# 1. Ensure conversation sub-folder (created_by = user_id)
# 2. Upload file to DMS (uploaded_by = user_id)
# 3. Return file_id + metadata
async def reference_external_file(
self, db, file_id: uuid.UUID
) -> dict:
"""Referenziert eine bereits im DMS existierende Datei."""
# Prüfe ob file_id existiert
# Return metadata ohne Kopie
async def get_file(self, db, file_id: uuid.UUID) -> File:
"""Holt eine Datei aus dem DMS."""
```
**Zwei Attachment-Modi:**
1. **`file_source='comm'`** — Datei wurde im Chat hochgeladen, liegt unter `_kommunikation/{conversation_id}/` im DMS
2. **`file_source='dms'`** — Datei ist eine Referenz auf ein bestehendes DMS-Dokument (keine Kopie)
### 3.7 Chat-interne RBAC
**Konversations-Rollen** (in `comm_participants.role`):
| Rolle | Rechte |
|---|---|
| `admin` | Alles: Nachrichten löschen, Teilnehmer verwalten, Titel ändern, andere admin machen |
| `member` | Nachrichten schreiben, lesen, reagieren, Dateien hochladen, @Mentions |
| `reader` | Nur lesen, reagieren — kein Schreiben |
**RBAC-Integration mit bestehendem System:**
```python
class CommRBAC:
"""Chat-interne RBAC, angebunden an bestehendes Permission-System."""
async def can_user_write(
self, db, user_id, conversation_id, user_permissions: list[str]
) -> bool:
"""Prüft ob User schreiben darf.
1. System-Permission 'comm:write' muss vorhanden sein
2. User muss Teilnehmer der Konversation sein
3. User-Rolle in Konversation muss 'admin' oder 'member' sein
4. Konversation darf nicht gelockt sein (für nicht-Plugins)
"""
async def can_user_manage(
self, db, user_id, conversation_id, user_permissions: list[str]
) -> bool:
"""Prüft ob User Teilnehmer verwalten darf.
1. System-Permission 'comm:manage' oder 'comm:admin'
2. User muss Konversation-Admin sein
"""
async def can_user_delete(
self, db, user_id, conversation_id, user_permissions: list[str]
) -> bool:
"""Prüft ob User Nachrichten/Konversation löschen darf.
1. System-Permission 'comm:delete' oder is_system_admin
2. Für eigene Nachrichten: immer erlaubt
3. Für fremde Nachrichten: Konversation-Admin
4. Für Konversation: admin + comm:delete
"""
async def invite_user(
self, db, conversation_id, inviter_id, invitee_id, role='member'
) -> comm_participants:
"""Lädt User in Konversation ein.
1. Inviter muss admin sein oder comm:manage haben
2. Invitee muss existieren und im selben Tenant sein
3. Füge als Participant hinzu
4. Event: participant.joined
"""
async def change_role(
self, db, conversation_id, changer_id, target_id, new_role
) -> comm_participants:
"""Ändert Rolle eines Teilnehmers.
1. Changer muss admin sein
2. Target muss Teilnehmer sein
3. Neue Rolle: 'admin', 'member', 'reader'
"""
```
**Permission-Flow:**
```
User Request → require_permission('comm:write') → System-Check
→ CommRBAC.can_user_write(current_user) → Konversations-Check
→ check_permission(current_user, 'comm:write') + is_system_admin
→ Participant role check → Erlaubt/Verweigert
```
### 3.8 WebSocket Manager
```python
class WebSocketManager:
"""Verwaltet WebSocket-Verbindungen pro User."""
def __init__(self):
self._connections: dict[str, list[WebSocket]] = {} # user_id → connections
async def connect(self, websocket: WebSocket, user_id: str):
"""Neue WebSocket-Verbindung."""
async def disconnect(self, websocket: WebSocket, user_id: str):
"""Verbindung geschlossen."""
async def send_to_user(self, user_id: str, message: dict):
"""Sendet Nachricht an alle Verbindungen eines Users."""
async def send_to_conversation(self, conversation_id: str, message: dict, exclude_user: str = None):
"""Sendet an alle Teilnehmer einer Konversation."""
async def broadcast(self, message: dict):
"""Broadcast an alle verbundenen User."""
```
**WebSocket Events:**
```
# Client → Server
{"type": "subscribe", "conversation_id": "..."}
{"type": "unsubscribe", "conversation_id": "..."}
{"type": "typing", "conversation_id": "...", "is_typing": true}
{"type": "ping"}
# Server → Client
{"type": "message.new", "conversation_id": "...", "message": {...}}
{"type": "message.updated", "message": {...}}
{"type": "message.deleted", "id": "...", "conversation_id": "..."}
{"type": "message.reaction", "message_id": "...", "emoji": "👍", "user_id": "...", "action": "add|remove"}
{"type": "participant.joined", "conversation_id": "...", "participant": {...}}
{"type": "participant.left", "conversation_id": "...", "participant_id": "..."}
{"type": "participant.role_changed", "conversation_id": "...", "participant_id": "...", "role": "..."}
{"type": "typing", "conversation_id": "...", "user_id": "...", "is_typing": true}
{"type": "conversation.updated", "conversation": {...}}
{"type": "conversation.pinned", "conversation_id": "...", "pinned": true}
{"type": "read.update", "conversation_id": "...", "user_id": "...", "last_read_msg_id": "..."}
{"type": "pong"}
```
### 3.9 Mini-App Registry
```python
class MiniAppRegistry:
"""Registry für Mini-Apps die von Plugins bereitgestellt werden."""
def __init__(self):
self._apps: dict[str, MiniAppDef] = {}
def register(self, app_id: str, name: str, icon: str,
render_schema: dict, handler: Callable) -> None:
"""Plugin registriert eine Mini-App."""
def unregister(self, app_id: str) -> None:
def list_apps(self) -> list[dict]:
"""Alle verfügbaren Mini-Apps für Frontend."""
def get_app(self, app_id: str) -> MiniAppDef | None:
class MiniAppDef(BaseModel):
app_id: str
name: str
icon: str
description: str
render_schema: dict # JSON-Schema für Frontend-Rendering
plugin_name: str # welches Plugin hat es registriert
```
### 3.10 Search Provider (unified_search Integration)
```python
class CommSearchProvider:
"""Provider für unified_search — durchsucht alle Konversationen und Nachrichten."""
async def search(
self, db, tenant_id, user_id, query: str, limit: int = 20
) -> list[dict]:
"""Sucht in Nachrichten und Konversationen.
Returns:
[
{
'type': 'message',
'id': '...',
'conversation_id': '...',
'conversation_title': '...',
'content': '...',
'sender_type': 'user',
'sender_name': 'Max',
'created_at': '...',
'snippet': '...matching text...'
},
{
'type': 'conversation',
'id': '...',
'title': '...',
'participant_count': 3,
'last_msg_at': '...'
}
]
"""
async def index_message(self, message: dict) -> None:
"""Indexiert eine Nachricht für die Suche."""
async def reindex_all(self, db, tenant_id) -> None:
"""Vollständige Neu-Indexierung."""
```
**Registrierung:** Das `kommunikation` Plugin registriert seinen Search Provider beim `unified_search` Plugin bei Aktivierung.
---
## 4. Plugin `ai_assistant` (Anpassung)
### 4.1 Was bleibt
- Provider-Verwaltung, Modelle, Presets, Agents, Tools
- Tool Registry (andere Plugins können Tools registrieren)
- Streaming-Logik
- AI Copilot System (AIConversation/AIMessage) — parallel laufen lassen
- Eigene Settings-Pages (Provider, Modelle, Agents)
### 4.2 Was ändert sich
- **Keine eigene Chat-UI** — `ChatWindow` wird durch `kommunikation` MessageSidebar ersetzt
- **Implementiert `ParticipantHandler`** — registriert sich als `ai` Teilnehmer
- **Erstellt gepinnten Raum "Assistent"** — 1:1 Konversation mit User + AI
- **Schreibt in `comm_messages`** — nicht mehr nur in `ai_chat_sessions`
- **Auf `message.received` hören** — wenn @KI erwähnt oder AI Teilnehmer → Response
- **Streaming über WebSocket** — nicht mehr SSE, sondern WebSocket `message.new` Events
- Token-basiertes Streaming: AI generiert Token für Token
- Pro Token: `{"type": "message.streaming", "conversation_id": "...", "message_id": "...", "token": "...", "chunk_index": N}`
- Am Ende: `{"type": "message.streaming.done", "message_id": "...", "final_content": "..."}`
- Frontend zeigt Token live an, ersetzt am Ende durch finalen Content
### 4.2.1 BasePlugin `services` Property (System-Fix)
`BasePlugin` wird um eine `services` Property erweitert, damit Plugins auf den `ServiceContainer` zugreifen können:
```python
# In app/plugins/base.py
class BasePlugin(ABC):
def __init__(self) -> None:
# ... bestehend ...
self._container: ServiceContainer | None = None
async def on_activate(self, db, service_container, event_bus) -> None:
self._container = service_container # ← NEU
# ... bestehend ...
@property
def services(self) -> ServiceContainer:
if self._container is None:
raise RuntimeError("Services not available — plugin not activated")
return self._container
```
**Aufwand:** 3-4 Zeilen in `base.py`. Keine bestehenden Plugins müssen geändert werden.
- Token-basiertes Streaming: AI generiert Token für Token
- Pro Token: `{"type": "message.streaming", "conversation_id": "...", "message_id": "...", "token": "...", "chunk_index": N}`
- Am Ende: `{"type": "message.streaming.done", "message_id": "...", "final_content": "..."}`
- Frontend zeigt Token live an, ersetzt am Ende durch finalen Content
### 4.3 ParticipantHandler Implementation
```python
class AIParticipantHandler(ParticipantHandler):
"""AI Assistant als Teilnehmer im kommunikation Plugin."""
async def on_message_received(
self, conversation_id, message, conversation, mentions, context
) -> list[dict] | None:
"""Reagiert auf Nachrichten in Konversationen mit AI-Teilnehmer."""
# 1. Prüfe ob AI Teilnehmer in dieser Konversation ist
# 2. Prüfe ob @KI erwähnt wurde ODER Konversation ist 1:1 mit AI
# 3. Wenn ja: generiere AI-Response via stream_chat()
# 4. Schreibe Response als comm_message (sender_type='ai')
# 5. Push via WebSocket
# 6. Return [response_message_dict]
def get_participant_info(self) -> dict:
return {
'display_name': 'KI Assistent',
'avatar_url': None, # Robot-Icon im Frontend
'capabilities': ['chat', 'tools', 'streaming'],
'description': 'KI Assistent mit LLM und Tools'
}
```
### 4.4 Plugin on_activate
```python
class AIAssistantPlugin(BasePlugin):
async def on_activate(self, db, container, event_bus):
await super().on_activate(db, container, event_bus)
# 1. Bei kommunikation registrieren
from app.plugins.builtins.kommunikation.participant_registry import get_participant_registry
registry = get_participant_registry()
registry.register('ai', AIParticipantHandler(self.services))
# 2. Auf message.received hören (für @KI Detection)
event_bus.subscribe('message.received', self.on_message_received)
# 3. Tools registrieren (bestehend)
# ...
async def on_deactivate(self, db, container, event_bus):
# Unregister participant
get_participant_registry().unregister('ai')
event_bus.unsubscribe('message.received', self.on_message_received)
await super().on_deactivate(db, container, event_bus)
```
---
## 5. Plugin `ai_proactive` (Anpassung)
### 5.1 Was bleibt
- Context-Engine (Kontext-Änderung → Suggestion)
- Background Jobs (deep_analysis via ARQ)
- Settings (Pro-User: enabled, categories, confidence, rate_limit, model)
- Tool Registry Integration
### 5.2 Was ändert sich
- **Keine eigene UI** — `SuggestionList` wird durch `kommunikation` MessageSidebar ersetzt
- **Implementiert `ParticipantHandler`** — registriert sich als `ai_proactive` Teilnehmer
- **Erstellt gepinnten Raum "Live KI"** — locked, gepinnt, für proaktive Vorschläge
- **Heartbeat** — regelmäßige Background-Job postet Status/Updates in den Raum
- **Schreibt in `comm_messages`** — nicht mehr in `ai_proactive_suggestions` (parallel)
- **Push via WebSocket** — nicht mehr SSE
- **Suggestion als Rich Content** — `action_card` Blocks mit Buttons
### 5.3 Heartbeat
```python
class AIProactivePlugin(BasePlugin):
manifest = PluginManifest(
...,
events=["context.view_changed", "context.entity_selected"],
...,
)
async def on_activate(self, db, container, event_bus):
await super().on_activate(db, container, event_bus)
# 1. Bei kommunikation registrieren
registry = get_participant_registry()
registry.register('ai_proactive', AIProactiveParticipantHandler(self.services))
# 2. Heartbeat Job registrieren (ARQ)
# Läuft alle X Minuten, postet Status in "Live KI" Raum
# z.B. "System aktiv — überwacht 3 Kontakte, 2 Vorschläge generiert"
async def on_deactivate(self, db, container, event_bus):
registry.unregister('ai_proactive')
# Heartbeat Job abmelden
await super().on_deactivate(db, container, event_bus)
```
**Heartbeat Job:**
```python
async def heartbeat_job(ctx):
"""Läuft alle 5 Minuten.
- Prüft ob Proactive AI für User aktiviert ist
- Postet Status-Message in "Live KI" Raum:
- Anzahl überwachter Entities
- Anzahl generierter Vorschläge (heute)
- System-Status (aktiv, pausiert, Fehler)
- Wenn neue Vorschläge vorhanden: pusht diese als action_card
"""
```
### 5.4 Proactive ParticipantHandler
```python
class AIProactiveParticipantHandler(ParticipantHandler):
async def on_message_received(
self, conversation_id, message, conversation, mentions, context
) -> list[dict] | None:
"""Reagiert auf Nachrichten im 'Live KI' Raum.
- User kann Fragen stellen ("Was schlägst du vor?")
- AI Proactive generiert Vorschlag basierend auf aktuellem Kontext
- Antwort als action_card mit Buttons
"""
def get_participant_info(self) -> dict:
return {
'display_name': 'Live KI',
'avatar_url': None, # Bulb-Icon im Frontend
'capabilities': ['proactive', 'context_aware', 'heartbeat'],
'description': 'Proaktive KI mit Kontext-Überwachung'
}
```
---
## 6. Plugin `system_notif` (Neu)
### 6.1 Verantwortung
Wandelt System-Events in Nachrichten um. Ersetzt langfristig das bestehende Notification-System.
### 6.2 Manifest
```python
PluginManifest(
name="system_notif",
version="1.0.0",
display_name="System Benachrichtigungen",
description="Wandelt System-Events in Chat-Nachrichten um",
dependencies=["kommunikation"],
routes=[], # Keine eigenen Routes — nur Participant
events=[
"lead.created", "contact.created", "contact.updated",
"task.overdue", "task.created", "mail.received",
"user.created", "workflow.completed",
],
migrations=["0001_initial.sql"],
permissions=["system_notif:read"],
is_core=False,
)
```
### 6.3 Funktionsweise
```python
class SystemNotificationPlugin(BasePlugin):
async def on_activate(self, db, container, event_bus):
await super().on_activate(db, container, event_bus)
# 1. Bei kommunikation registrieren
registry = get_participant_registry()
registry.register('system', SystemParticipantHandler())
# 2. Auf System-Events hören (bereits via manifest.events)
# 3. Für jeden User: Erstelle gepinnten Raum "System" (locked)
async def on_lead_created(self, payload):
"""lead.created → System-Nachricht"""
# 1. Finde alle User die benachrichtigt werden sollen
# 2. Für jeden User: schreibe Nachricht in System-Raum
# 3. Nachricht als action_card: "Neuer Lead: Acme Corp" + [Öffnen] Button
async def on_contact_created(self, payload):
"""contact.created → System-Nachricht"""
async def on_task_overdue(self, payload):
"""task.overdue → System-Nachricht (warning)"""
```
### 6.4 System ParticipantHandler
```python
class SystemParticipantHandler(ParticipantHandler):
async def on_message_received(
self, conversation_id, message, conversation, mentions, context
) -> list[dict] | None:
"""System ist passiv — liest nur, antwortet nicht.
Ausnahme: User kann auf Action-Buttons reagieren (archivieren, öffnen).
Diese Reaktionen werden als metadata auf der Nachricht gesetzt, nicht als neue Nachricht.
"""
return None
def get_participant_info(self) -> dict:
return {
'display_name': 'System',
'avatar_url': None, # Bell-Icon im Frontend
'capabilities': ['notifications', 'action_cards'],
'description': 'System-Benachrichtigungen'
}
```
### 6.5 Migration bestehender Notifications
- Bestehende `notifications` Tabelle bleibt erhalten (Abwärtskompatibilität)
- Neue System-Events schreiben in `comm_messages` (System-Raum)
- Langfristig: Frontend zeigt nur noch `comm_messages` an, alte Tabelle wird deprecated
- `create_notification()` bleibt unverändert (Core-Code kann Plugin-Code nicht importieren)
- Stattdessen: `create_notification()` publiziert EventBus Event `notification.created`
- `system_notif` Plugin hört auf `notification.created` und schreibt in `comm_messages`
- Keine zirkuläre Abhängigkeit — Core → EventBus → Plugin
---
## 7. Frontend — MessageSidebar
### 7.1 Struktur
Die rechte Sidebar wird zur **MessageSidebar** und ersetzt die aktuelle AISidebar:
```
┌──────────────────────────────────────────┐
│ Kommunikation [×] │
├──────────────────────────────────────────┤
│ 🔍 Suche... │
├──────────────────────────────────────────┤
│ RAUM-LISTE │
│ 📌 🔒 System │ ← gepinnt + locked (Plugin)
│ 🔔 3 neue Leads importiert │
│ 📌 🔒 Live KI │ ← gepinnt + locked (Plugin)
│ 🤖 System aktiv — 2 Vorschläge... │
│ 📌 🔒 Assistent │ ← gepinnt + locked (Plugin)
│ 🤖 47 Kontakte ohne Email... │
│ ──────────────────────────── │
│ 👥 Projekt Alpha │ ← User-Raum (nicht gepinnt)
│ Max: @KI fasse die Leads zusammen │
│ 👥 Sales Team │
│ Lisa: Q3-Zahlen sind da │
│ 👤 Max Müller │ ← Direkt-Chat
│ Du: Hast du kurz Zeit? │
├──────────────────────────────────────────┤
│ FEED (aktive Konversation) │
│ ┌────────────────────────────────────┐ │
│ │ Max: @KI fasse die Leads zusammen │ │
│ │ 🤖 KI: 3 neue Leads, 2 aus... │ │
│ │ Lisa: Super, danke! │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ 📎 lead_report.pdf │ │ │
│ │ │ ──────────────────────────── │ │ │
│ │ │ ## 3 neue Leads │ │ │
│ │ │ - **Acme Corp** — €50k │ │ │
│ │ │ ──────────────────────────── │ │ │
│ │ │ [Öffnen] [Archivieren] │ │ │
│ │ └──────────────────────────────┘ │ │
│ └────────────────────────────────────┘ │
├──────────────────────────────────────────┤
│ [📎] [Eingabefeld...] [Senden] │
└──────────────────────────────────────────┘
```
### 7.2 Komponenten
```
frontend/src/components/comm/
├── MessageSidebar.tsx # Haupt-Komponente (ersetzt AISidebar)
├── ConversationList.tsx # Raum-Liste (links im Panel)
├── ConversationItem.tsx # Einzelne Raum-Zeile
├── MessageFeed.tsx # Nachrichten-Feed (Mitte)
├── MessageBubble.tsx # Einzelne Nachricht
├── MessageInput.tsx # Eingabefeld + Upload + @Mention
├── BlockRenderer.tsx # Rich Content Block Renderer
├── blocks/
│ ├── MarkdownBlock.tsx
│ ├── HtmlBlock.tsx
│ ├── ImageBlock.tsx
│ ├── AudioBlock.tsx
│ ├── VideoBlock.tsx
│ ├── FileBlock.tsx
│ ├── ActionCardBlock.tsx
│ ├── ContactCardBlock.tsx
│ └── MiniAppBlock.tsx
├── ParticipantAvatars.tsx # Teilnehmer-Avatare im Header
├── ReactionBar.tsx # Emoji-Reaktionen
├── TypingIndicator.tsx # "X tippt..."
└── CreateConversationDialog.tsx # Neue Konversation erstellen
```
### 7.3 commStore (neu)
```typescript
// store/commStore.ts — neuer Store für Communication State
import { create } from 'zustand';
export interface CommState {
conversations: Conversation[];
activeConversationId: string | null;
messages: Record<string, Message[]>; // conversation_id → messages
typingUsers: Record<string, string[]>; // conversation_id → user_ids
unreadCounts: Record<string, number>; // conversation_id → count
setConversations: (convs: Conversation[]) => void;
setActiveConversation: (id: string | null) => void;
addMessage: (convId: string, msg: Message) => void;
updateConversation: (conv: Conversation) => void;
setTyping: (convId: string, userIds: string[]) => void;
setUnread: (convId: string, count: number) => void;
}
export const useCommStore = create<CommState>((set) => ({
conversations: [],
activeConversationId: null,
messages: {},
typingUsers: {},
unreadCounts: {},
// ... implementations
}));
```
### 7.4 uiStore Anpassung
```typescript
// uiStore.ts — neue State-Struktur
export type MessageSidebarView = 'rooms' | 'feed';
export interface UIState {
// ... bestehende ...
// Communication
messageSidebarCollapsed: boolean;
messageSidebarView: MessageSidebarView; // 'rooms' = Liste, 'feed' = aktive Konversation
activeConversationId: string | null;
commWebSocket: WebSocket | null;
toggleMessageSidebar: () => void;
setMessageSidebarView: (view: MessageSidebarView) => void;
setActiveConversation: (id: string | null) => void;
}
```
### 7.4 WebSocket Hook
```typescript
// hooks/useCommWebSocket.ts
export function useCommWebSocket() {
const { activeConversationId, addMessage, updateConversation } = useCommStore();
useEffect(() => {
const ws = new WebSocket(`${WS_BASE}/api/v1/comm/ws`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'message.new':
addMessage(data.conversation_id, data.message);
break;
case 'conversation.updated':
updateConversation(data.conversation);
break;
case 'typing':
// Update typing indicator
break;
// ...
}
};
return () => ws.close();
}, []);
}
```
### 7.5 API Client
```typescript
// api/comm.ts
export const commApi = {
// Konversationen
listConversations: () => api.get('/comm/conversations'),
createConversation: (data) => api.post('/comm/conversations', data),
getConversation: (id) => api.get(`/comm/conversations/${id}`),
updateConversation: (id, data) => api.patch(`/comm/conversations/${id}`, data),
pinConversation: (id) => api.post(`/comm/conversations/${id}/pin`),
unpinConversation: (id) => api.delete(`/comm/conversations/${id}/pin`),
muteConversation: (id) => api.post(`/comm/conversations/${id}/mute`),
// Teilnehmer
addParticipant: (convId, data) => api.post(`/comm/conversations/${convId}/participants`, data),
removeParticipant: (convId, pid) => api.delete(`/comm/conversations/${convId}/participants/${pid}`),
changeRole: (convId, pid, role) => api.patch(`/comm/conversations/${convId}/participants/${pid}`, { role }),
// Nachrichten
getMessages: (convId, page) => api.get(`/comm/conversations/${convId}/messages`, { params: { page } }),
sendMessage: (convId, data) => api.post(`/comm/conversations/${convId}/messages`, data),
editMessage: (id, data) => api.patch(`/comm/messages/${id}`, data),
deleteMessage: (id) => api.delete(`/comm/messages/${id}`),
// Attachments
uploadAttachment: (msgId, file) => {
const formData = new FormData();
formData.append('file', file);
return api.post(`/comm/messages/${msgId}/attachments`, formData);
},
referenceDmsFile: (msgId, fileId) => api.post(`/comm/messages/${msgId}/attachments`, { file_id: fileId }),
// Reaktionen
addReaction: (msgId, emoji) => api.post(`/comm/messages/${msgId}/reactions`, { emoji }),
removeReaction: (msgId, emoji) => api.delete(`/comm/messages/${msgId}/reactions/${emoji}`),
// Read State
markRead: (convId) => api.post(`/comm/conversations/${convId}/read`),
// Mini-Apps
listMiniApps: () => api.get('/comm/miniapps'),
startMiniApp: (convId, appId, config) => api.post(`/comm/conversations/${convId}/miniapps`, { app_id: appId, config }),
};
```
---
## 8. REST API
### 8.1 Konversationen
```
GET /api/v1/comm/conversations
→ Liste für aktuellen User (inkl. pinned, locked, unread_count, last_msg)
Query: ?archived=false (default: nur nicht-archivierte)
Response: [{ id, title, is_locked, locked_by, is_direct, is_pinned,
participants: [...], last_msg_preview, last_msg_sender_type,
last_msg_at, unread_count }]
POST /api/v1/comm/conversations
Body: { title?, participant_ids: [uuid], participant_types: ['user'],
initial_message?, is_direct? }
→ Neue Konversation erstellen (User wird automatisch admin)
Permission: comm:create
GET /api/v1/comm/conversations/{id}
→ Details + Teilnehmer-Liste + Rolle des aktuellen Users
PATCH /api/v1/comm/conversations/{id}
Body: { title?, is_archived? }
→ Titel ändern (nicht wenn locked), archivieren
Permission: comm:write + admin role (für title)
DELETE /api/v1/comm/conversations/{id}
→ Konversation löschen (admin) oder verlassen (member)
Permission: comm:delete (admin) oder comm:write (leave)
POST /api/v1/comm/conversations/{id}/pin
→ Für aktuellen User anpinnen
Permission: comm:read
DELETE /api/v1/comm/conversations/{id}/pin
→ Pinning entfernen
POST /api/v1/comm/conversations/{id}/mute
→ Stummschalten für aktuellen User
DELETE /api/v1/comm/conversations/{id}/mute
```
### 8.2 Teilnehmer
```
POST /api/v1/comm/conversations/{id}/participants
Body: { participant_id, participant_type: 'user', role: 'member' }
→ Teilnehmer hinzufügen (einladen)
Permission: comm:manage + admin role
DELETE /api/v1/comm/conversations/{id}/participants/{pid}
→ Teilnehmer entfernen
Permission: comm:manage + admin role (oder self-leave)
PATCH /api/v1/comm/conversations/{id}/participants/{pid}
Body: { role: 'admin'|'member'|'reader' }
→ Rolle ändern
Permission: comm:admin + admin role
```
### 8.3 Nachrichten
```
GET /api/v1/comm/conversations/{id}/messages
→ Nachrichten (paginiert, 50 pro Seite)
Query: ?page=1&before={msg_id}
Response: { items: [...], total, page, has_more }
Permission: comm:read + participant
POST /api/v1/comm/conversations/{id}/messages
Body: { content, content_format: 'markdown',
blocks?: [{ block_type, block_data }],
reply_to_id?, attachments?: [{ file_id?, file_source? }] }
→ Nachricht senden
Permission: comm:write + participant (admin/member role)
→ Triggert EventBus: message.received
→ Triggert ParticipantHandler für alle nicht-user Teilnehmer
→ Push via WebSocket an alle Teilnehmer
PATCH /api/v1/comm/messages/{id}
Body: { content?, read? }
→ Bearbeiten oder als gelesen markieren
Permission: comm:write (eigene) oder comm:manage (fremde)
DELETE /api/v1/comm/messages/{id}
→ Nachricht löschen (soft delete)
Permission: comm:delete (eigene) oder admin role
```
### 8.4 Attachments
```
POST /api/v1/comm/messages/{id}/attachments
Body: multipart/form-data (file) ODER JSON ({ file_id, file_source: 'dms' })
→ Datei hochladen (→ DMS Bridge) oder DMS-Datei referenzieren
Permission: comm:write + participant
GET /api/v1/comm/attachments/{id}
→ Datei herunterladen
Permission: comm:read + participant
GET /api/v1/comm/attachments/{id}/thumbnail
→ Thumbnail für Bild/Video
```
### 8.5 Reaktionen
```
POST /api/v1/comm/messages/{id}/reactions
Body: { emoji }
Permission: comm:write + participant
DELETE /api/v1/comm/messages/{id}/reactions/{emoji}
```
### 8.6 Read State
```
POST /api/v1/comm/conversations/{id}/read
Body: { last_read_msg_id }
→ Lese-Status aktualisieren
Permission: comm:read + participant
```
### 8.7 Mini-Apps
```
GET /api/v1/comm/miniapps
→ Verfügbare Mini-Apps (von Plugins registriert)
Response: [{ app_id, name, icon, description, plugin_name }]
POST /api/v1/comm/conversations/{id}/miniapps
Body: { app_id, config? }
→ Mini-App in Konversation starten → erzeugt miniapp Block
Permission: comm:write + participant
```
### 8.8 WebSocket
```
WS /api/v1/comm/ws
→ Authentifiziert via Session-Cookie (wie REST-API, Same-Origin)
→ Bei Connect: Session validieren, user_id extrahieren, Tenant-Kontext setzen
→ Bei ungültiger Session: WebSocket mit Code 4001 geschlossen
→ Siehe WebSocket Events oben
```
---
## 9. Plugin-übergreifende Integration
### 9.1 EventBus Flow bei neuer Nachricht
```
1. User schreibt Nachricht → POST /api/v1/comm/conversations/{id}/messages
2. kommunikation Service:
a. Speichert Nachricht in comm_messages + comm_message_blocks
b. Publiziert EventBus: message.received { conversation_id, message, mentions }
c. Pusht via WebSocket an alle Teilnehmer
3. ParticipantHandler werden aufgerufen:
a. AI ParticipantHandler: @KI erwähnt? → generiere Response → neue comm_message
b. AI Proactive Handler: im Live KI Raum? → generiere Vorschlag → neue comm_message
c. System Handler: passiv → return None
4. Wenn neue Nachricht von ParticipantHandler: → wieder Schritt 2
WICHTIG: Infinite-Loop-Protection
- Jede Nachricht bekommt metadata['triggered_by'] = original_message_id
- ParticipantHandler prüft: wenn message.id == triggered_by → nicht erneut triggern
- Max depth counter in metadata['trigger_depth'] (default 0, max 3)
- Bei max depth: Nachricht wird gespeichert aber keine Handler mehr getriggert
```
### 9.2 Plugin-Raum-Erstellung bei Aktivierung
```
Plugin wird aktiviert → on_activate()
→ Prüfe ob Plugin-Raum für User existiert
→ Wenn nicht: Erstelle Konversation
- title: "System" / "Live KI" / "Assistent"
- is_locked: true
- locked_by: plugin_name
- created_by_type: 'plugin'
- Füge Plugin als Participant hinzu (participant_type)
- Füge User als Participant hinzu (role: 'reader' für system, 'member' für ai)
- Pinne für User
→ Wenn ja: nichts tun
```
### 9.3 unified_search Integration
```
kommunikation Plugin aktiviert →
→ Registriert CommSearchProvider bei unified_search
→ unified_search indexiert comm_messages
→ Suche liefert Ergebnisse aus Konversationen
```
### 9.4 DMS Integration
```
User lädt Datei im Chat →
→ DmsBridge.store_attachment()
→ Erstellt/Findet DMS-Ordner _kommunikation/{conversation_id}/
→ Speichert Datei im DMS
→ Erstellt comm_message_attachment mit file_id + file_source='comm'
User referenziert DMS-Datei →
→ DmsBridge.reference_external_file(file_id)
→ Prüft DMS-Datei existiert
→ Erstellt comm_message_attachment mit file_id + file_source='dms'
→ Keine Kopie, nur Referenz
```
---
## 10. Vollständige Checkliste — Was berücksichtigt wurde
### Core Features
- [x] Einheitliches Messaging-System (ein Plugin, ein Datenmodell)
- [x] KI als Teilnehmer (kein eigener Chat-Typ)
- [x] System als Teilnehmer (Notifications = Nachrichten)
- [x] Externe Messenger als Teilnehmer (erweiterbar)
- [x] Rich Content Transport (Blocks: text, markdown, html, image, audio, video, file, action_card, contact_card, miniapp)
- [x] Mini-Apps (Plugin-basiert, im Chat renderbar)
- [x] Datei-Upload (DMS-Integration mit eigener Datenquelle + externe Referenzen)
- [x] WebSocket (Real-time für alles)
- [x] EventBus Integration (Plugins reagieren auf Nachrichten)
### Raum-Logik
- [x] Räume = benannte Konversationen (Titel editierbar)
- [x] Pinning (user-spezifisch, pro User)
- [x] Locking (Plugin-Lock: User kann nicht ändern)
- [x] Plugin-erstellte Räume (System, Live KI, Assistent)
- [x] Archivierung (ausgeblendete Konversationen)
- [x] Stummschaltung (mute pro User)
### KI-Plugins
- [x] ai_assistant als Teilnehmer (ohne eigene Chat-UI)
- [x] ai_proactive als Teilnehmer (ohne eigene UI)
- [x] Heartbeat für ai_proactive (regelmäßiger Background-Job)
- [x] Gepinnter Kanal für proaktive KI
- [x] @KI Mention in jedem Chat
- [x] Streaming über WebSocket
- [x] AI Copilot bleibt parallel (nicht migriert)
### RBAC
- [x] Anbindung an bestehendes Permission-System (comm:read, comm:write, etc.)
- [x] Konversations-Rollen (admin, member, reader)
- [x] User einladen (admin only)
- [x] Rollen vergeben (admin only)
- [x] System-Permission + Konversations-Permission (zwei-Level Check)
- [x] Locked Räume (User kann Titel/Teilnehmer nicht ändern)
### DMS
- [x] Eigener DMS-Bereich (_kommunikation/{conversation_id}/)
- [x] Externe DMS-Referenzen (file_source='dms')
- [x] Thumbnail-Generierung für Bilder/Videos
### Suche
- [x] unified_search Provider für comm_messages + comm_conversations
- [x] Volltext-Suche in Nachrichten
- [x] Konversations-Suche
### Frontend
- [x] MessageSidebar ersetzt AISidebar
- [x] Raum-Liste (pinned/locked oben, dann normale)
- [x] Unified Feed mit Rich Content Rendering
- [x] Eingabefeld mit Datei-Upload + @Mention
- [x] WebSocket-Verbindung
- [x] Block-Renderer (Markdown, HTML, Image, Audio, Video, File, ActionCard, MiniApp)
- [x] Reaktionen (Emoji-Bar)
- [x] Typing-Indikator
- [x] Lese-Status (gelesen-Häkchen)
### Migration
- [x] Alte Tabellen bleiben (ai_chat_sessions, ai_conversations, notifications)
- [x] Neue Tabellen parallel (comm_*)
- [x] Schrittweise Migration
- [x] Abwärtskompatibilität
### Zukünftig
- [x] Messenger-Gateway Plugins (WhatsApp, Telegram, Email)
- [x] Push-Notifications für Mobile (später)
- [x] E2E-Verschlüsselung (später evaluieren)
---
## 11. Was könnte noch fehlen? — Ergänzungen
### 11.1 Message Threading
- `reply_to_id` ist vorhanden für direkte Antworten
- Echte Thread-Ansicht (Sub-Threads) wäre möglich, aber nicht in Phase 1
- Empfehlung: Reply-To für Phase 1, Sub-Threads später
### 11.2 Message Drafts
- Drafts pro Konversation speichern (lokal im Frontend oder serverseitig)
- Empfehlung: Lokal im Frontend (localStorage) für Phase 1
### 11.3 Message Forwarding
- Nachricht an andere Konversation weiterleiten
- Empfehlung: Später, einfache Implementierung (kopiere message + blocks)
### 11.4 Conversation Export
- Konversation als PDF/CSV exportieren
- Empfehlung: Später, über Report Generator Plugin
### 11.5 User Presence / Online-Status
- Real-time Online-Status über WebSocket
- Empfehlung: Phase 2, über WebSocket presence channel
### 11.6 Voice Messages
- Audio-Block ist vorhanden, Aufnahme im Frontend
- Empfehlung: Phase 2 (Audio-Block + Frontend Recorder)
### 11.7 Message Pinning (innerhalb Konversation)
- `is_pinned` auf comm_messages vorhanden
- Wichtige Nachrichten im Feed anpinnen
- Empfehlung: Phase 1 (Feld vorhanden, UI später)
### 11.8 Conversation Description / Topic
- Raum-Beschreibung / Thema
- Empfehlung: In `metadata` speichern, UI später
### 11.9 Read Receipts (Detail)
- Wer hat die Nachricht gelesen?
- Empfehlung: Phase 2 (comm_message_reads reicht für Phase 1)
### 11.10 Message Search (innerhalb Konversation)
- Suche innerhalb einer Konversation
- Empfehlung: Frontend-Filter auf geladene Nachrichten + Server-Suche für ältere
### 11.11 Notification Preferences pro Konversation
- Stummschaltung ist vorhanden (mute)
- Feinere Einstellungen (nur @Mentions, etc.)
- Empfehlung: Phase 2
### 11.12 Group Avatar / Icon
- Konversations-Icon setzen
- Empfehlung: In `metadata.icon` speichern, Frontend später
### 11.13 Nachrichten-Bearbeitung-Historie
- Neue Tabelle `comm_message_edits` speichert alte Versionen bei Bearbeitung
- Felder: id, message_id, old_content, old_blocks (JSONB), edited_by, edited_at
- Audit-Trail: jede Bearbeitung wird nachvollziehbar
### 11.14 Rate-Limiting (optionale Konfiguration)
- Konfigurierbares Rate-Limit pro Tenant (z.B. max N Nachrichten pro Minute pro User)
- In den Einstellungen aktivierbar/deaktivierbar
- Default: deaktiviert, kann bei Bedarf eingeschaltet werden
### 11.15 Datei-Größen-Limit und Referenz-Modus
- Dateien < 100MB: direkter Upload im Chat → DMS unter `_kommunikation/{conversation_id}/`
- Dateien >= 100MB: nur DMS-Referenz (`file_source='dms'`) — kein Upload im Chat
- Frontend zeigt bei großen Dateien einen "Im DMS öffnen" Link statt Download
- `comm_message_attachments.file_source` unterscheidet die Modi
### 11.16 Konversations-Cover-Bild
- Gruppen-Chats / Räume können ein Cover-Bild haben
- Gespeichert in `metadata.cover_url`
- Frontend zeigt Cover-Bild in Raum-Liste und Konversations-Header
### 11.17 WebSocket Reconnection
- Frontend auto-reconnect bei Verbindungsabbruch
- Exponential backoff (1s, 2s, 4s, 8s, max 30s)
- Bei Reconnect: Lese-Status synchronisieren, verpasste Nachrichten nachladen
### 11.18 Offline-Queue (später für Mobile)
- Nachrichten lokal speichern bei Offline-Status
- Bei Reconnect automatisch senden
- Nur für Mobile App relevant, nicht für Desktop
### 11.19 Datenretention (Einstellungen)
- Keine automatische Löschung in Phase 1
- Einstellungen pro Tenant: Aufbewahrungszeit konfigurierbar (z.B. 90 Tage, 1 Jahr, unbegrenzt)
- Bereits in Phase 1 als Setting-Feld vorsehen, Funktionalität später
### 11.20 Konversations-Limit (Einstellungen)
- Kein Limit in Phase 1
- Einstellungen pro Tenant: maximale Anzahl Konversationen konfigurierbar
- Bereits in Phase 1 als Setting-Feld vorsehen, Funktionalität später
---
## 12. Implementierungs-Phasen
### Phase 1: Backend — Plugin `kommunikation` (Woche 1-2)
1. Plugin-Gerüst (plugin.py, manifest, __init__.py)
2. Datenmodell (models.py) — alle Tabellen
3. Migration (0001_initial.sql)
4. Participant Registry (participant_registry.py)
5. Services (services.py) — CRUD, Nachrichten senden, Raum-Erstellung
6. RBAC (rbac.py) — Konversations-Rollen, Permission-Checks
7. DMS Bridge (dms_bridge.py) — Attachment-Speicherung
8. WebSocket Manager (websocket_manager.py)
9. REST API (routes.py) — alle Endpoints
10. Mini-App Registry (miniapp_registry.py)
11. Content Types (content_types.py) — Block-Definitionen
12. Search Provider (search_provider.py)
### Phase 2: Backend — AI Plugins anpassen (Woche 2-3)
1. ai_assistant: ParticipantHandler implementieren
2. ai_assistant: Bei kommunikation registrieren
3. ai_assistant: message.received → AI-Response
4. ai_assistant: Streaming über WebSocket
5. ai_assistant: Gepinnten Raum "Assistent" erstellen
6. ai_proactive: ParticipantHandler implementieren
7. ai_proactive: Bei kommunikation registrieren
8. ai_proactive: Heartbeat Job
9. ai_proactive: Gepinnten Raum "Live KI" erstellen
10. ai_proactive: Suggestion als action_card
### Phase 3: Backend — Plugin `system_notif` (Woche 3)
1. Plugin-Gerüst
2. ParticipantHandler (passiv)
3. Event-Handler (lead.created, contact.created, etc.)
4. Gepinnten Raum "System" erstellen
5. Bestehende Notifications migrieren (parallel)
### Phase 4: Frontend — MessageSidebar (Woche 3-4)
1. MessageSidebar-Komponente (ersetzt AISidebar)
2. ConversationList (Raum-Liste mit Pinning/Locking)
3. MessageFeed (Nachrichten-Feed)
4. MessageInput (Eingabefeld + Upload + @Mention)
5. WebSocket Hook
6. API Client (comm.ts)
7. uiStore Anpassung
8. Alte AISidebar entfernen
### Phase 5: Frontend — Rich Content Renderer (Woche 4-5)
1. BlockRenderer Haupt-Komponente
2. MarkdownBlock, HtmlBlock
3. ImageBlock, AudioBlock, VideoBlock, FileBlock
4. ActionCardBlock (mit Button-Handler)
5. ContactCardBlock
6. MiniAppBlock (Plugin-basiert)
7. ReactionBar
8. TypingIndicator
9. ParticipantAvatars
### Phase 5.5: Daten-Migration bestehender Chats (Woche 5)
1. Migration-Script: ai_chat_sessions → comm_conversations (1:1 mit AI)
2. Migration-Script: ai_chat_messages → comm_messages (sender_type='user'/'ai')
3. Migration-Script: ai_chat_attachments → comm_message_attachments
4. Migration-Script: notifications → comm_messages (System-Raum, sender_type='system')
5. Migration-Script: ai_proactive_suggestions → comm_messages (Live KI Raum, als action_card)
6. Flag in metadata: `{"migrated_from": "ai_chat_sessions"}` für Nachverfolgung
7. Alte Tabellen bleiben erhalten (kein Datenverlust)
### Phase 6: Testing & Deployment (Woche 5)
1. Backend Tests (API, WebSocket, RBAC, Participant Registry)
2. Frontend Tests (MessageSidebar, Block Renderer)
3. Integration Tests (AI Response, System Notifications)
4. Deployment (Coolify)
### Phase 7: Messenger-Gateway Plugins (später)
1. whatsapp_gateway Plugin
2. telegram_gateway Plugin
3. email_gateway Plugin
### Phase 8: Mobile & Push (später)
1. Push-Notification Service
2. Mobile App API
3. E2E-Verschlüsselung (evaluieren)
---
## 13. Zusammenfassung
```
Ein Plugin (kommunikation) → Chat-Infrastruktur + Rich Content + WebSocket + RBAC + DMS
Ein Interface (ParticipantHandler) → Plugins docken als Teilnehmer an
Ein Datenmodell (10 Tabellen) → Conversations, Participants, Messages, Blocks, Attachments, Reactions, Reads, Pins
Eine UI (MessageSidebar) → Raum-Liste + Feed + Eingabefeld
Eine WebSocket → Real-time für alles
Ein EventBus → Plugins reagieren auf Nachrichten
KI = Teilnehmer → @KI in jedem Chat, keine eigene UI
Proactive KI = Teilnehmer → Heartbeat + gepinnter Kanal, keine eigene UI
System = Teilnehmer → Notifications als Nachrichten, locked Raum
WhatsApp = Teilnehmer → Externe Messenger andocken (später)
Mini-Apps = Plugin-Blocks → Erweiterbar im Chat
Räume = Benannte Chats → Titel + Pinning + Locking
RBAC = Zwei-Level → System-Permission + Konversations-Rolle
DMS = Bridge → Eigener Bereich + externe Referenzen
Suche = Provider → unified_search Integration
```