KI-AGENT: Matrix-Einbettung stabilisieren

This commit is contained in:
2026-05-18 18:56:37 +02:00
parent 7c68ce61f2
commit c93ea4284d
5 changed files with 129 additions and 5 deletions

View File

@@ -35,6 +35,11 @@ type MatrixUserSession = {
validUntilMs: number
}
type MatrixLoginTokenResponse = {
login_token: string
expires_in_ms: number
}
type MatrixTenantRoomOptions = {
key?: string
name?: string
@@ -1146,6 +1151,40 @@ export function matrixService(server: FastifyInstance) {
}
}
const createElementRoomSession = async (
userId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {}
) => {
const room = await provisionTenantRoom(userId, tenantId, options)
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
roomId: room.roomId,
alias: room.alias,
})
const token = await requestMatrixJson<MatrixLoginTokenResponse>(
"/_matrix/client/v1/login/get_token",
session.accessToken,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
}
)
return {
roomId: room.roomId,
alias: room.alias,
key: room.key,
name: room.name,
matrixUserId: session.matrixUserId,
loginToken: token.login_token,
expiresInMs: token.expires_in_ms,
homeserverUrl: homeserverUrl(),
serverName: serverName(),
}
}
const listTenantCommunicationUsers = async (tenantId: number | null) => {
const tenant = await getCurrentTenant(tenantId)
const rows = await server.db
@@ -1302,6 +1341,7 @@ export function matrixService(server: FastifyInstance) {
getTenantRoomMessages,
getTenantRoomMembers,
sendTenantRoomMessage,
createElementRoomSession,
syncTenantRoomMembers,
getGeneralRoomMessages,
getGeneralRoomMembers,

View File

@@ -125,6 +125,17 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
server.post("/communication/matrix/rooms/general/session", async (req, reply) => {
try {
return await matrix.createElementRoomSession(req.user.user_id, req.user.tenant_id, {
key: "allgemein",
name: "Allgemeiner Chat",
})
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix session failed")
}
})
server.post("/communication/matrix/rooms/general/members/sync", async (req, reply) => {
try {
return await matrix.syncTenantRoomMembers(req.user.user_id, req.user.tenant_id, {
@@ -190,6 +201,18 @@ export default async function communicationRoutes(server: FastifyInstance) {
}
})
server.post("/communication/matrix/rooms/:roomKey/session", async (req, reply) => {
try {
return await matrix.createElementRoomSession(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req)
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix session failed")
}
})
server.post("/communication/matrix/rooms/:roomKey/members/sync", async (req, reply) => {
try {
return await matrix.syncTenantRoomMembers(

View File

@@ -14,6 +14,8 @@ const matrixMessagesViewport = ref(null)
const roomCreateOpen = ref(false)
const matrixCallOpen = ref(false)
const matrixCallMode = ref("video")
const matrixCallLoading = ref(false)
const matrixCallUrl = ref("")
const roomCreateForm = ref({
name: "",
key: "",
@@ -73,6 +75,10 @@ const matrixCallTitle = computed(() =>
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
)
const activeMatrixCallUrl = computed(() =>
matrixCallUrl.value || activeRoomElementUrl.value
)
const roomCreateKeyPreview = computed(() =>
normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name)
)
@@ -340,7 +346,18 @@ const syncRoomMembers = async () => {
}
}
const openMatrixCall = (mode = "video") => {
const buildElementRoomSessionUrl = (session) => {
const roomAddress = session.roomId || session.alias || activeRoomMatrixAddress.value
if (!matrixElementUrl.value || !roomAddress) return ""
const params = new URLSearchParams({
loginToken: session.loginToken
})
return `${matrixElementUrl.value}/?${params.toString()}#/room/${encodeURIComponent(roomAddress)}`
}
const openMatrixCall = async (mode = "video") => {
if (!canStartMatrixCall.value) {
toast.add({
title: "Besprechung kann noch nicht gestartet werden",
@@ -350,7 +367,24 @@ const openMatrixCall = (mode = "video") => {
}
matrixCallMode.value = mode
matrixCallLoading.value = true
matrixCallOpen.value = true
try {
const session = await $api(`${activeRoomEndpoint.value}/session`, {
method: "POST"
})
matrixCallUrl.value = buildElementRoomSessionUrl(session) || activeRoomElementUrl.value
} catch (error) {
matrixCallUrl.value = activeRoomElementUrl.value
toast.add({
title: "Matrix-Anmeldung konnte nicht vorbereitet werden",
description: "Der Raum wird ohne automatische Anmeldung geöffnet.",
color: "warning"
})
} finally {
matrixCallLoading.value = false
}
}
const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
@@ -619,6 +653,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
color="neutral"
variant="outline"
aria-label="Audioanruf starten"
:loading="matrixCallLoading && matrixCallMode === 'audio'"
:disabled="!canStartMatrixCall"
@click="openMatrixCall('audio')"
/>
@@ -627,6 +662,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
color="primary"
variant="soft"
aria-label="Videokonferenz starten"
:loading="matrixCallLoading && matrixCallMode === 'video'"
:disabled="!canStartMatrixCall"
@click="openMatrixCall('video')"
/>
@@ -893,6 +929,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
color="neutral"
variant="outline"
block
:loading="matrixCallLoading && matrixCallMode === 'audio'"
:disabled="!canStartMatrixCall"
@click="openMatrixCall('audio')"
>
@@ -903,6 +940,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
color="primary"
variant="soft"
block
:loading="matrixCallLoading && matrixCallMode === 'video'"
:disabled="!canStartMatrixCall"
@click="openMatrixCall('video')"
>
@@ -940,8 +978,8 @@ onBeforeUnmount(stopMatrixAutoRefresh)
</div>
<div class="flex items-center gap-2">
<UButton
v-if="activeRoomElementUrl"
:to="activeRoomElementUrl"
v-if="activeMatrixCallUrl"
:to="activeMatrixCallUrl"
target="_blank"
icon="i-heroicons-arrow-top-right-on-square"
color="neutral"
@@ -959,10 +997,16 @@ onBeforeUnmount(stopMatrixAutoRefresh)
</header>
<div class="min-h-0 flex-1 bg-muted">
<div
v-if="matrixCallLoading"
class="flex h-full items-center justify-center text-sm text-muted"
>
Matrix wird geladen...
</div>
<iframe
v-if="canStartMatrixCall"
v-else-if="canStartMatrixCall"
:key="`${activeRoomKey}-${matrixCallMode}`"
:src="activeRoomElementUrl"
:src="activeMatrixCallUrl"
class="h-full w-full border-0"
allow="camera; microphone; display-capture; clipboard-read; clipboard-write; fullscreen; autoplay"
referrerpolicy="no-referrer"

View File

@@ -184,3 +184,14 @@ Das Backend stellt geschützte Matrix-Endpunkte unter `/api/communication/matrix
Für lokale Provisionierung muss `MATRIX_REGISTRATION_SHARED_SECRET` aus `matrix/dev/synapse/homeserver.yaml` in der Backend-Umgebung gesetzt werden. Die lokale Synapse-Konfiguration ist absichtlich nicht versioniert, weil sie Secrets enthält.
In der lokalen Entwicklung liest das Backend dieses Secret als Fallback direkt aus `matrix/dev/synapse/homeserver.yaml`, sofern `NODE_ENV` nicht `production` ist. Auf Servern muss das Secret weiterhin explizit über die Umgebung oder das Secret-Management gesetzt werden.
Für den eingebetteten Element-Login in FEDEO muss in der lokalen Synapse-Konfiguration außerdem der kurzlebige Login-Token-Flow aktiv sein:
```yaml
login_via_existing_session:
enabled: true
require_ui_auth: false
token_timeout: "5m"
```
Nach einer Änderung an `matrix/dev/synapse/homeserver.yaml` muss `matrix-dev-synapse` neu gestartet werden.

View File

@@ -5,6 +5,12 @@
"server_name": "localhost"
}
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "http://localhost:8081"
}
],
"disable_custom_urls": false,
"disable_guests": true,
"brand": "FEDEO Matrix Dev",