cc3ac9a43d
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
1634 lines
66 KiB
Markdown
1634 lines
66 KiB
Markdown
# 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
|
||
```
|