KI-AGENT: Matrix-Anrufe im Chat vorbereiten

This commit is contained in:
2026-05-18 18:38:21 +02:00
parent f6dd37b458
commit 7c68ce61f2
5 changed files with 198 additions and 0 deletions

View File

@@ -89,6 +89,8 @@ MATRIX_DEV_TURN_MAX_PORT=49200
# Backend-Integration gegen den lokalen Matrix-Stack
MATRIX_HOMESERVER_URL=http://localhost:8008
MATRIX_RTC_JWT_URL=http://localhost:8081
MATRIX_LIVEKIT_URL=ws://localhost:7880
MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-dev-synapse-homeserver-yaml
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
NUXT_PUBLIC_MATRIX_ELEMENT_URL=http://localhost:8080

View File

@@ -129,6 +129,25 @@ export function matrixService(server: FastifyInstance) {
readLocalDevRegistrationSharedSecret() ||
""
const rtcHost = () =>
process.env.MATRIX_RTC_HOST ||
secrets.MATRIX_RTC_HOST ||
"call.fedeo.de"
const rtcJwtUrl = () =>
process.env.MATRIX_RTC_JWT_URL ||
secrets.MATRIX_RTC_JWT_URL ||
(process.env.NODE_ENV === "production"
? `https://${rtcHost()}/livekit/jwt`
: `http://localhost:${process.env.MATRIX_DEV_RTC_JWT_PORT || "8081"}`)
const livekitUrl = () =>
process.env.MATRIX_LIVEKIT_URL ||
secrets.MATRIX_LIVEKIT_URL ||
(process.env.NODE_ENV === "production"
? `wss://${rtcHost()}/livekit/sfu`
: `ws://localhost:${process.env.MATRIX_DEV_LIVEKIT_PORT || "7880"}`)
const serviceUserLocalpart = () =>
process.env.MATRIX_SERVICE_USER_LOCALPART ||
secrets.MATRIX_SERVICE_USER_LOCALPART ||
@@ -404,6 +423,13 @@ export function matrixService(server: FastifyInstance) {
serverName: serverName(),
provisioningConfigured: Boolean(registrationSharedSecret()),
reachable: false,
calls: {
provider: "matrixrtc-livekit",
configured: Boolean(rtcJwtUrl() && livekitUrl()),
rtcHost: rtcHost(),
rtcJwtUrl: rtcJwtUrl(),
livekitUrl: livekitUrl(),
},
}
}
@@ -418,6 +444,13 @@ export function matrixService(server: FastifyInstance) {
serverName: serverName(),
provisioningConfigured: Boolean(registrationSharedSecret()),
reachable: true,
calls: {
provider: "matrixrtc-livekit",
configured: Boolean(rtcJwtUrl() && livekitUrl()),
rtcHost: rtcHost(),
rtcJwtUrl: rtcJwtUrl(),
livekitUrl: livekitUrl(),
},
versions: versions.versions,
}
} catch (err: any) {
@@ -427,6 +460,13 @@ export function matrixService(server: FastifyInstance) {
serverName: serverName(),
provisioningConfigured: Boolean(registrationSharedSecret()),
reachable: false,
calls: {
provider: "matrixrtc-livekit",
configured: Boolean(rtcJwtUrl() && livekitUrl()),
rtcHost: rtcHost(),
rtcJwtUrl: rtcJwtUrl(),
livekitUrl: livekitUrl(),
},
error: err.message,
}
}

View File

@@ -40,6 +40,9 @@ export let secrets = {
STIRLING_API_KEY: string
MATRIX_HOMESERVER_URL?: string
MATRIX_SERVER_NAME?: string
MATRIX_RTC_HOST?: string
MATRIX_RTC_JWT_URL?: string
MATRIX_LIVEKIT_URL?: string
MATRIX_REGISTRATION_SHARED_SECRET?: string
MATRIX_SERVICE_USER_LOCALPART?: string
}
@@ -76,6 +79,9 @@ const secretKeys = [
"STIRLING_API_KEY",
"MATRIX_HOMESERVER_URL",
"MATRIX_SERVER_NAME",
"MATRIX_RTC_HOST",
"MATRIX_RTC_JWT_URL",
"MATRIX_LIVEKIT_URL",
"MATRIX_REGISTRATION_SHARED_SECRET",
"MATRIX_SERVICE_USER_LOCALPART",
] as const

View File

@@ -1,6 +1,7 @@
<script setup>
const toast = useToast()
const { $api } = useNuxtApp()
const runtimeConfig = useRuntimeConfig()
const status = ref(null)
const identity = ref(null)
@@ -11,6 +12,8 @@ const matrixMembers = ref([])
const matrixMessageDraft = ref("")
const matrixMessagesViewport = ref(null)
const roomCreateOpen = ref(false)
const matrixCallOpen = ref(false)
const matrixCallMode = ref("video")
const roomCreateForm = ref({
name: "",
key: "",
@@ -48,6 +51,28 @@ const activeRoomEndpoint = computed(() =>
`/api/communication/matrix/rooms/${encodeURIComponent(activeRoomKey.value)}`
)
const matrixElementUrl = computed(() =>
String(runtimeConfig.public?.matrixElementUrl || "").replace(/\/+$/, "")
)
const activeRoomMatrixAddress = computed(() =>
activeRoom.value?.roomId || activeRoom.value?.alias || ""
)
const activeRoomElementUrl = computed(() => {
if (!matrixElementUrl.value || !activeRoomMatrixAddress.value) return ""
return `${matrixElementUrl.value}/#/room/${encodeURIComponent(activeRoomMatrixAddress.value)}`
})
const canStartMatrixCall = computed(() =>
Boolean(canUseMatrixChat.value && activeRoom.value?.exists && activeRoomElementUrl.value)
)
const matrixCallTitle = computed(() =>
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
)
const roomCreateKeyPreview = computed(() =>
normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name)
)
@@ -315,6 +340,19 @@ const syncRoomMembers = async () => {
}
}
const openMatrixCall = (mode = "video") => {
if (!canStartMatrixCall.value) {
toast.add({
title: "Besprechung kann noch nicht gestartet werden",
color: "warning"
})
return
}
matrixCallMode.value = mode
matrixCallOpen.value = true
}
const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
await loadRoomMessages({ silent })
@@ -576,6 +614,22 @@ onBeforeUnmount(stopMatrixAutoRefresh)
</div>
<div class="flex items-center gap-2">
<UButton
icon="i-heroicons-phone"
color="neutral"
variant="outline"
aria-label="Audioanruf starten"
:disabled="!canStartMatrixCall"
@click="openMatrixCall('audio')"
/>
<UButton
icon="i-heroicons-video-camera"
color="primary"
variant="soft"
aria-label="Videokonferenz starten"
:disabled="!canStartMatrixCall"
@click="openMatrixCall('video')"
/>
<UButton
class="lg:hidden"
to="/communication"
@@ -829,6 +883,34 @@ onBeforeUnmount(stopMatrixAutoRefresh)
</div>
</div>
<div>
<h4 class="mb-2 text-xs font-medium uppercase text-muted">
Besprechung
</h4>
<div class="grid grid-cols-2 gap-2">
<UButton
icon="i-heroicons-phone"
color="neutral"
variant="outline"
block
:disabled="!canStartMatrixCall"
@click="openMatrixCall('audio')"
>
Audio
</UButton>
<UButton
icon="i-heroicons-video-camera"
color="primary"
variant="soft"
block
:disabled="!canStartMatrixCall"
@click="openMatrixCall('video')"
>
Video
</UButton>
</div>
</div>
<div>
<h4 class="mb-2 text-xs font-medium uppercase text-muted">
Eigene Identität
@@ -839,5 +921,67 @@ onBeforeUnmount(stopMatrixAutoRefresh)
</div>
</div>
</aside>
<UModal
v-model:open="matrixCallOpen"
fullscreen
class="h-[100dvh]"
>
<template #content>
<div class="flex h-[100dvh] flex-col bg-default">
<header class="flex shrink-0 items-center justify-between gap-3 border-b border-default px-4 py-3">
<div class="min-w-0">
<h3 class="truncate text-base font-semibold text-highlighted">
{{ matrixCallTitle }} · {{ activeRoom.name }}
</h3>
<p class="truncate text-xs text-muted">
{{ activeRoomMatrixAddress }}
</p>
</div>
<div class="flex items-center gap-2">
<UButton
v-if="activeRoomElementUrl"
:to="activeRoomElementUrl"
target="_blank"
icon="i-heroicons-arrow-top-right-on-square"
color="neutral"
variant="outline"
>
Extern öffnen
</UButton>
<UButton
icon="i-heroicons-x-mark"
color="neutral"
variant="ghost"
@click="matrixCallOpen = false"
/>
</div>
</header>
<div class="min-h-0 flex-1 bg-muted">
<iframe
v-if="canStartMatrixCall"
:key="`${activeRoomKey}-${matrixCallMode}`"
:src="activeRoomElementUrl"
class="h-full w-full border-0"
allow="camera; microphone; display-capture; clipboard-read; clipboard-write; fullscreen; autoplay"
referrerpolicy="no-referrer"
/>
<div
v-else
class="flex h-full items-center justify-center p-6"
>
<UAlert
icon="i-heroicons-exclamation-triangle"
color="warning"
variant="soft"
title="Besprechung nicht verfügbar"
description="Der Matrix-Raum oder die Element-Integration ist noch nicht bereit."
/>
</div>
</div>
</div>
</template>
</UModal>
</div>
</template>

View File

@@ -45,6 +45,12 @@ const statusItems = computed(() => [
value: status.value?.reachable ? "Erreichbar" : "Nicht erreichbar",
icon: status.value?.reachable ? "i-heroicons-signal" : "i-heroicons-signal-slash",
color: status.value?.reachable ? "success" : "error"
},
{
label: "Audio/Video",
value: status.value?.calls?.configured ? "Bereit" : "Nicht eingerichtet",
icon: status.value?.calls?.configured ? "i-heroicons-video-camera" : "i-heroicons-video-camera-slash",
color: status.value?.calls?.configured ? "success" : "warning"
}
])