KI-AGENT: Mobile Matrix-Kommunikation vollständig integrieren

This commit is contained in:
2026-05-20 20:38:48 +02:00
parent 2278dfa714
commit 3796bc2953
5 changed files with 1245 additions and 10 deletions

View File

@@ -183,6 +183,100 @@ type RequestOptions = {
body?: unknown;
};
export type MatrixStatus = {
enabled?: boolean;
ready?: boolean;
configured?: boolean;
homeserverUrl?: string | null;
[key: string]: unknown;
};
export type MatrixIdentity = {
matrixUserId: string;
displayName?: string | null;
};
export type MatrixRoom = {
key: string;
name: string;
topic?: string | null;
type?: 'room' | 'project' | 'direct' | string;
group?: string;
roomId?: string | null;
alias?: string | null;
exists?: boolean;
projectId?: number;
projectNumber?: string | null;
userId?: string;
email?: string | null;
entityType?: string | null;
entityId?: number | null;
entityUuid?: string | null;
unread?: number;
mentions?: number;
provisionEndpoint?: string;
[key: string]: unknown;
};
export type MatrixAttachment = {
fileName?: string | null;
mimeType?: string | null;
size?: number | null;
mxcUri?: string | null;
previewUrl?: string | null;
downloadUrl?: string | null;
};
export type MatrixReaction = {
key: string;
count?: number;
own?: boolean;
senders?: string[];
[key: string]: unknown;
};
export type MatrixMessage = {
id: string;
sender: string;
senderDisplayName?: string | null;
body?: string | null;
timestamp?: string | number | null;
own?: boolean;
edited?: boolean;
redacted?: boolean;
msgtype?: string;
attachment?: MatrixAttachment | null;
replyToEventId?: string | null;
reactions?: MatrixReaction[];
[key: string]: unknown;
};
export type MatrixMember = {
matrixUserId: string;
displayName?: string | null;
avatarUrl?: string | null;
membership?: string;
[key: string]: unknown;
};
export type MatrixUser = {
userId: string;
matrixUserId: string;
displayName?: string | null;
email?: string | null;
[key: string]: unknown;
};
export type MatrixSyncResponse = {
nextBatch?: string;
messages?: MatrixMessage[];
replacements?: MatrixMessage[];
reactions?: (MatrixReaction & { targetEventId?: string })[];
redactions?: { redacts?: string; eventId?: string; targetEventId?: string }[];
members?: MatrixMember[];
[key: string]: unknown;
};
function buildUrl(path: string): string {
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
@@ -227,10 +321,231 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
return payload as T;
}
async function apiFormRequest<T>(path: string, token: string, formData: FormData): Promise<T> {
const response = await fetch(buildUrl(path), {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
body: formData,
});
const payload = await parseJson(response);
if (!response.ok) {
const message =
(payload as { message?: string; error?: string } | null)?.message ||
(payload as { message?: string; error?: string } | null)?.error ||
`Request failed (${response.status}) for ${path}`;
throw new Error(message);
}
return payload as T;
}
function matrixRoomPath(roomKey: string, suffix = ''): string {
return `/api/communication/matrix/rooms/${encodeURIComponent(roomKey)}${suffix}`;
}
export async function checkBackendHealth(): Promise<{ status: string; [key: string]: unknown }> {
return apiRequest<{ status: string; [key: string]: unknown }>('/health');
}
export async function fetchMatrixStatus(token: string): Promise<MatrixStatus> {
return apiRequest<MatrixStatus>('/api/communication/matrix/status', { token });
}
export async function fetchMatrixIdentity(token: string): Promise<MatrixIdentity> {
return apiRequest<MatrixIdentity>('/api/communication/matrix/me', { token });
}
export async function provisionMatrixUser(token: string): Promise<MatrixIdentity> {
return apiRequest<MatrixIdentity>('/api/communication/matrix/me/provision', {
method: 'POST',
token,
});
}
export async function fetchMatrixRooms(token: string): Promise<MatrixRoom[]> {
const [rooms, projectRooms, directRooms, unread] = await Promise.all([
apiRequest<{ rooms?: MatrixRoom[] }>('/api/communication/matrix/rooms', { token }),
apiRequest<{ rooms?: MatrixRoom[] }>('/api/communication/matrix/project-rooms', { token }),
apiRequest<{ rooms?: MatrixRoom[] }>('/api/communication/matrix/direct-rooms', { token }),
apiRequest<{ rooms?: Record<string, { count?: number; mentions?: number }> }>('/api/communication/matrix/unread', {
token,
}),
]);
const unreadByRoom = unread.rooms || {};
const decorate = (room: MatrixRoom, group: string): MatrixRoom => ({
...room,
group,
unread: unreadByRoom[room.key]?.count || 0,
mentions: unreadByRoom[room.key]?.mentions || 0,
});
return [
...(rooms.rooms || []).map((room) => decorate(room, 'Räume')),
...(projectRooms.rooms || []).map((room) => decorate(room, 'Projekte')),
...(directRooms.rooms || []).map((room) => decorate(room, 'Direkt')),
];
}
export async function fetchMatrixUsers(token: string): Promise<MatrixUser[]> {
const response = await apiRequest<{ users?: MatrixUser[] }>('/api/communication/matrix/users', { token });
return response.users || [];
}
export async function createMatrixRoom(
token: string,
payload: { key: string; name: string; topic?: string | null; type?: string }
): Promise<MatrixRoom> {
return apiRequest<MatrixRoom>('/api/communication/matrix/rooms', {
method: 'POST',
token,
body: payload,
});
}
export async function provisionMatrixRoom(token: string, room: MatrixRoom): Promise<MatrixRoom> {
if (room.provisionEndpoint) {
return apiRequest<MatrixRoom>(room.provisionEndpoint, { method: 'POST', token });
}
if (room.type === 'project' && room.projectId) {
return apiRequest<MatrixRoom>(`/api/communication/matrix/project-rooms/${room.projectId}/provision`, {
method: 'POST',
token,
});
}
if (room.type === 'direct' && room.userId) {
return apiRequest<MatrixRoom>(`/api/communication/matrix/direct-rooms/${encodeURIComponent(room.userId)}/provision`, {
method: 'POST',
token,
});
}
return apiRequest<MatrixRoom>(matrixRoomPath(room.key, '/provision'), {
method: 'POST',
token,
body: {
key: room.key,
name: room.name,
topic: room.topic,
type: room.type || 'room',
entityType: room.entityType,
entityId: room.entityId,
entityUuid: room.entityUuid,
},
});
}
export async function fetchMatrixMessages(token: string, roomKey: string): Promise<MatrixMessage[]> {
const response = await apiRequest<{ messages?: MatrixMessage[] }>(matrixRoomPath(roomKey, '/messages'), { token });
return response.messages || [];
}
export async function syncMatrixRoom(
token: string,
roomKey: string,
since?: string,
initial = false
): Promise<MatrixSyncResponse> {
const query = new URLSearchParams();
if (since) query.set('since', since);
if (initial) query.set('initial', '1');
const suffix = query.toString() ? `/sync?${query.toString()}` : '/sync';
return apiRequest<MatrixSyncResponse>(matrixRoomPath(roomKey, suffix), { token });
}
export async function fetchMatrixMembers(token: string, roomKey: string): Promise<MatrixMember[]> {
const response = await apiRequest<{ members?: MatrixMember[] }>(matrixRoomPath(roomKey, '/members'), { token });
return response.members || [];
}
export async function sendMatrixMessage(
token: string,
roomKey: string,
text: string,
replyToEventId?: string | null
): Promise<MatrixMessage> {
return apiRequest<MatrixMessage>(matrixRoomPath(roomKey, '/messages'), {
method: 'POST',
token,
body: { text, replyToEventId },
});
}
export async function editMatrixMessage(token: string, roomKey: string, eventId: string, text: string): Promise<MatrixMessage> {
return apiRequest<MatrixMessage>(matrixRoomPath(roomKey, `/messages/${encodeURIComponent(eventId)}`), {
method: 'PUT',
token,
body: { text },
});
}
export async function deleteMatrixMessage(token: string, roomKey: string, eventId: string): Promise<void> {
await apiRequest(matrixRoomPath(roomKey, `/messages/${encodeURIComponent(eventId)}`), {
method: 'DELETE',
token,
});
}
export async function reactToMatrixMessage(token: string, roomKey: string, eventId: string, key: string): Promise<void> {
await apiRequest(matrixRoomPath(roomKey, `/messages/${encodeURIComponent(eventId)}/reactions`), {
method: 'POST',
token,
body: { key },
});
}
export async function markMatrixRoomRead(token: string, roomKey: string, eventId?: string): Promise<void> {
await apiRequest(matrixRoomPath(roomKey, '/read'), {
method: 'POST',
token,
body: { eventId },
});
}
export async function syncMatrixMembers(token: string, roomKey: string): Promise<void> {
await apiRequest(matrixRoomPath(roomKey, '/members/sync'), {
method: 'POST',
token,
});
}
export async function inviteMatrixMember(token: string, roomKey: string, userId: string): Promise<void> {
await apiRequest(matrixRoomPath(roomKey, '/members/invite'), {
method: 'POST',
token,
body: { userId },
});
}
export async function removeMatrixMember(token: string, roomKey: string, matrixUserId: string): Promise<void> {
await apiRequest(matrixRoomPath(roomKey, `/members/${encodeURIComponent(matrixUserId)}`), {
method: 'DELETE',
token,
});
}
export async function uploadMatrixAttachment(
token: string,
roomKey: string,
file: { uri: string; name: string; mimeType?: string | null }
): Promise<MatrixMessage> {
const formData = new FormData();
formData.append('file', {
uri: file.uri,
name: file.name,
type: file.mimeType || 'application/octet-stream',
} as unknown as Blob);
return apiFormRequest<MatrixMessage>(matrixRoomPath(roomKey, '/attachments'), token, formData);
}
export async function renderPrintLabel(
token: string,
context: Record<string, unknown>,