# 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; // conversation_id → messages typingUsers: Record; // conversation_id → user_ids unreadCounts: Record; // 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((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 ```