feat: add KeywordCommandDetector to all 19 Matrix bots

All bots now support natural language commands via KeywordCommandDetector:
- matrix-chat-bot (gespraeche, modelle, verlauf, etc.)
- matrix-mana-bot (todo, timer, kalender, summary, etc.)
- matrix-manadeck-bot (decks, karten, lernen, mana, etc.)
- matrix-planta-bot (pflanzen, giessen, faellig, etc.)
- matrix-presi-bot (presis, folien, themes, teilen, etc.)
- matrix-project-doc-bot (projekte, generate, export, etc.)
- matrix-questions-bot (fragen, recherche, antwort, etc.)
- matrix-skilltree-bot (skills, xp, stats, aktivitaeten, etc.)
- matrix-stats-bot (stats, heute, woche, realtime, etc.)
- matrix-storage-bot (dateien, ordner, teilen, suche, etc.)
- matrix-tts-bot (voice, voices, speed, etc.)

All bots include COMMON_KEYWORDS (hilfe, help, status).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-01 03:26:25 +01:00
parent c28410b736
commit a23430f210
14 changed files with 261 additions and 36 deletions

View file

@ -7,7 +7,10 @@ export const userLists = pgTable('user_lists', {
description: text('description'),
quoteIds: jsonb('quote_ids').$type<string[]>().default([]), // References static quote IDs from shared package
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
});
export type UserList = typeof userLists.$inferSelect;

View file

@ -37,7 +37,7 @@ export class ListService {
): Promise<UserList> {
const [list] = await this.db
.update(userLists)
.set({ ...data, updatedAt: new Date() })
.set(data)
.where(and(eq(userLists.id, listId), eq(userLists.userId, userId)))
.returning();

View file

@ -1,3 +1,27 @@
{
"extends": "@manacore/shared-tsconfig/nestjs"
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"outDir": "./dist",
"baseUrl": "./",
"rootDir": "./src",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -1,12 +1,28 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common';
import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { ChatService, Conversation } from '../chat/chat.service';
import { SessionService } from '@manacore/bot-services';
import { HELP_MESSAGE, BRANCH_ICONS } from '../config/configuration';
@Injectable()
export class MatrixService extends BaseMatrixService {
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['gespraeche', 'conversations', 'liste', 'chats'], command: 'gespraeche' },
{ keywords: ['modelle', 'models', 'ki modelle', 'ai models'], command: 'modelle' },
{ keywords: ['neu', 'new', 'neues gespraech', 'new conversation'], command: 'neu' },
{ keywords: ['verlauf', 'history', 'nachrichten', 'messages'], command: 'verlauf' },
{ keywords: ['archiviert', 'archived', 'archiv liste'], command: 'archiviert' },
{ keywords: ['chat', 'fragen', 'ask', 'frage'], command: 'chat' },
]);
constructor(
configService: ConfigService,
private chatService: ChatService,
@ -67,6 +83,12 @@ export class MatrixService extends BaseMatrixService {
message: string,
sender: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(message);
if (keywordCommand) {
message = `!${keywordCommand}`;
}
if (!message.startsWith('!')) return;
const [command, ...args] = message.slice(1).split(/\s+/);

View file

@ -1,4 +1,5 @@
import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
import { KeywordCommandDetector, COMMON_KEYWORDS } from '@manacore/matrix-bot-common';
import { AiHandler } from '../handlers/ai.handler';
import { TodoHandler } from '../handlers/todo.handler';
import { CalendarHandler } from '../handlers/calendar.handler';
@ -21,22 +22,20 @@ interface CommandRoute {
description: string;
}
// Natural language keywords (German + English)
const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [
{ keywords: ['hilfe', 'help', 'was kannst du', 'befehle'], command: '!help' },
{ keywords: ['modelle', 'models', 'welche modelle'], command: '!models' },
{
keywords: ['meine aufgaben', 'zeige aufgaben', 'todo liste', 'was muss ich'],
command: '!list',
},
{ keywords: ['heute', 'was steht heute an'], command: '!today' },
{ keywords: ['termine', 'kalender', 'meine termine'], command: '!cal' },
{ keywords: ['timer', 'stoppuhr'], command: '!timers' },
{ keywords: ['zusammenfassung', 'wie war mein tag', 'tagesrückblick'], command: '!summary' },
];
@Injectable()
export class CommandRouterService {
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['modelle', 'models', 'welche modelle', 'ai models'], command: 'models' },
{ keywords: ['meine aufgaben', 'zeige aufgaben', 'todo liste', 'was muss ich', 'aufgaben'], command: 'list' },
{ keywords: ['heute', 'was steht heute an', 'today'], command: 'today' },
{ keywords: ['termine', 'kalender', 'meine termine', 'calendar'], command: 'cal' },
{ keywords: ['timer', 'stoppuhr', 'zeitmesser'], command: 'timers' },
{ keywords: ['zusammenfassung', 'wie war mein tag', 'tagesrueckblick', 'summary'], command: 'summary' },
{ keywords: ['todo', 'aufgabe', 'neue aufgabe', 'task'], command: 'todo' },
{ keywords: ['alarm', 'wecker', 'alarme'], command: 'alarms' },
{ keywords: ['clear', 'loeschen', 'verlauf loeschen', 'reset'], command: 'clear' },
]);
private readonly logger = new Logger(CommandRouterService.name);
private routes: CommandRoute[] = [];
@ -262,20 +261,11 @@ export class CommandRouterService {
}
private detectKeywordCommand(message: string): string | null {
const lowerMessage = message.toLowerCase().trim();
// Only check short messages
if (lowerMessage.length > 60) return null;
for (const { keywords, command } of KEYWORD_COMMANDS) {
for (const keyword of keywords) {
if (lowerMessage === keyword || lowerMessage.includes(keyword)) {
this.logger.debug(`Detected keyword "${keyword}" -> "${command}"`);
return command;
}
}
const command = this.keywordDetector.detect(message);
if (command) {
this.logger.debug(`Detected keyword -> "!${command}"`);
return `!${command}`;
}
return null;
}

View file

@ -5,6 +5,8 @@ import {
MatrixBotConfig,
MatrixRoomEvent,
UserListMapper,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { ManadeckService, Deck, Card } from '../manadeck/manadeck.service';
import { SessionService } from '@manacore/bot-services';
@ -17,6 +19,19 @@ export class MatrixService extends BaseMatrixService {
private cardsMapper = new UserListMapper<Card>();
private currentDeckId: Map<string, string> = new Map();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['decks', 'meine decks', 'kartendecks', 'liste'], command: 'decks' },
{ keywords: ['karten', 'cards', 'meine karten'], command: 'karten' },
{ keywords: ['lernen', 'study', 'ueben', 'wiederholen'], command: 'lernen' },
{ keywords: ['faellig', 'due', 'anstehend', 'zu lernen'], command: 'faellig' },
{ keywords: ['mana', 'credits', 'guthaben', 'punkte'], command: 'mana' },
{ keywords: ['stats', 'statistik', 'fortschritt', 'statistiken'], command: 'stats' },
{ keywords: ['generieren', 'generate', 'erstellen', 'ai'], command: 'generate' },
{ keywords: ['featured', 'empfohlen', 'beliebte decks'], command: 'featured' },
{ keywords: ['rangliste', 'leaderboard', 'bestenliste'], command: 'leaderboard' },
]);
constructor(
configService: ConfigService,
private manadeckService: ManadeckService,
@ -40,6 +55,12 @@ export class MatrixService extends BaseMatrixService {
message: string,
sender: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(message);
if (keywordCommand) {
message = `!${keywordCommand}`;
}
if (!message.startsWith('!')) return;
const parts = message.slice(1).split(/\s+/);

View file

@ -1,6 +1,13 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent, UserListMapper } from '@manacore/matrix-bot-common';
import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
UserListMapper,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { PlantaService, Plant } from '../planta/planta.service';
import { SessionService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
@ -10,6 +17,16 @@ export class MatrixService extends BaseMatrixService {
// Store last shown plants per user for reference by number
private plantsMapper = new UserListMapper<Plant>();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['pflanzen', 'plants', 'meine pflanzen', 'liste'], command: 'pflanzen' },
{ keywords: ['giessen', 'water', 'bewaessern', 'wasser geben'], command: 'giessen' },
{ keywords: ['faellig', 'due', 'anstehend', 'upcoming'], command: 'faellig' },
{ keywords: ['neu', 'new', 'neue pflanze', 'add'], command: 'neu' },
{ keywords: ['historie', 'history', 'verlauf', 'giess historie'], command: 'historie' },
{ keywords: ['intervall', 'interval', 'frequenz', 'wie oft'], command: 'intervall' },
]);
// Field mappings for edit command
private readonly fieldMappings: Record<string, string> = {
name: 'name',
@ -52,6 +69,12 @@ export class MatrixService extends BaseMatrixService {
event: MatrixRoomEvent,
body: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(body);
if (keywordCommand) {
body = `!${keywordCommand}`;
}
if (!body.startsWith('!')) return;
const sender = event.sender;

View file

@ -5,6 +5,8 @@ import {
MatrixBotConfig,
MatrixRoomEvent,
UserListMapper,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { PresiService, Deck, Theme, SlideContent } from '../presi/presi.service';
import { SessionService } from '@manacore/bot-services';
@ -16,6 +18,16 @@ export class MatrixService extends BaseMatrixService {
private decksMapper = new UserListMapper<Deck>();
private themesMapper = new UserListMapper<Theme>();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['presis', 'decks', 'praesentationen', 'liste'], command: 'presis' },
{ keywords: ['folien', 'slides', 'folie hinzufuegen'], command: 'folie' },
{ keywords: ['themes', 'designs', 'vorlagen', 'stile'], command: 'themes' },
{ keywords: ['teilen', 'share', 'freigeben', 'link'], command: 'teilen' },
{ keywords: ['links', 'shares', 'freigaben', 'geteilte'], command: 'links' },
{ keywords: ['neu', 'new', 'neue praesentation', 'erstellen'], command: 'neu' },
]);
constructor(
configService: ConfigService,
private presiService: PresiService,
@ -40,6 +52,12 @@ export class MatrixService extends BaseMatrixService {
event: MatrixRoomEvent,
body: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(body);
if (keywordCommand) {
body = `!${keywordCommand}`;
}
if (!body.startsWith('!')) return;
const sender = event.sender;

View file

@ -1,6 +1,12 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common';
import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { ProjectService } from '../project/project.service';
import { MediaService } from '../media/media.service';
import { GenerationService } from '../generation/generation.service';
@ -13,6 +19,17 @@ export class MatrixService extends BaseMatrixService {
// Active project per user (matrixUserId -> projectId)
private activeProjects: Map<string, string> = new Map();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['projekte', 'projects', 'meine projekte', 'liste'], command: 'projects' },
{ keywords: ['archiv', 'archive', 'archivieren'], command: 'archive' },
{ keywords: ['generieren', 'generate', 'erstellen', 'blogbeitrag'], command: 'generate' },
{ keywords: ['exportieren', 'export', 'herunterladen', 'download'], command: 'export' },
{ keywords: ['stile', 'styles', 'vorlagen', 'formate'], command: 'styles' },
{ keywords: ['neu', 'new', 'neues projekt', 'projekt starten'], command: 'new' },
{ keywords: ['wechseln', 'switch', 'umschalten'], command: 'switch' },
]);
constructor(
configService: ConfigService,
private projectService: ProjectService,
@ -77,6 +94,12 @@ export class MatrixService extends BaseMatrixService {
body: string,
sender: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(body);
if (keywordCommand) {
body = `!${keywordCommand}`;
}
if (body.startsWith('!')) {
await this.handleCommand(roomId, sender, body);
} else {

View file

@ -1,6 +1,13 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent, UserListMapper } from '@manacore/matrix-bot-common';
import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
UserListMapper,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { QuestionsService, Question, Collection, Answer } from '../questions/questions.service';
import { SessionService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
@ -12,6 +19,17 @@ export class MatrixService extends BaseMatrixService {
private collectionsMapper = new UserListMapper<Collection>();
private answersMapper = new UserListMapper<Answer>();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['fragen', 'questions', 'meine fragen', 'liste'], command: 'fragen' },
{ keywords: ['recherche', 'research', 'suchen', 'untersuchen'], command: 'recherche' },
{ keywords: ['antwort', 'answer', 'antworten', 'ergebnis'], command: 'antwort' },
{ keywords: ['quellen', 'sources', 'referenzen', 'links'], command: 'quellen' },
{ keywords: ['sammlungen', 'collections', 'ordner', 'kategorien'], command: 'sammlungen' },
{ keywords: ['suche', 'search', 'finde', 'durchsuchen'], command: 'suche' },
{ keywords: ['neu', 'new', 'neue frage', 'frage stellen'], command: 'neu' },
]);
constructor(
configService: ConfigService,
private questionsService: QuestionsService,
@ -34,6 +52,12 @@ export class MatrixService extends BaseMatrixService {
event: MatrixRoomEvent,
body: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(body);
if (keywordCommand) {
body = `!${keywordCommand}`;
}
if (!body.startsWith('!')) return;
const sender = event.sender;

View file

@ -5,6 +5,8 @@ import {
MatrixBotConfig,
MatrixRoomEvent,
UserListMapper,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { SkilltreeService, Skill, SkillBranch } from '../skilltree/skilltree.service';
import { SessionService } from '@manacore/bot-services';
@ -15,6 +17,15 @@ export class MatrixService extends BaseMatrixService {
// User list mapper for number-based reference
private skillsMapper = new UserListMapper<Skill>();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['skills', 'faehigkeiten', 'meine skills', 'liste'], command: 'skills' },
{ keywords: ['xp', 'punkte', 'erfahrung', 'erfahrungspunkte'], command: 'xp' },
{ keywords: ['stats', 'statistik', 'statistiken', 'fortschritt'], command: 'stats' },
{ keywords: ['aktivitaeten', 'activities', 'verlauf', 'historie'], command: 'aktivitaeten' },
{ keywords: ['neu', 'new', 'neuer skill', 'skill erstellen'], command: 'neu' },
]);
// Branch name mappings (German/English)
private readonly branchMappings: Record<string, SkillBranch> = {
intellect: 'intellect',
@ -64,6 +75,12 @@ export class MatrixService extends BaseMatrixService {
event: MatrixRoomEvent,
body: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(body);
if (keywordCommand) {
body = `!${keywordCommand}`;
}
if (!body.startsWith('!')) return;
const sender = event.sender;

View file

@ -1,6 +1,12 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common';
import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { AnalyticsService } from '../analytics/analytics.service';
import { UsersService } from '../users/users.service';
@ -8,6 +14,15 @@ import { UsersService } from '../users/users.service';
export class MatrixService extends BaseMatrixService {
private reportRoomId: string = '';
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['stats', 'statistik', 'statistiken', 'uebersicht'], command: 'stats' },
{ keywords: ['heute', 'today', 'tagesstatistik'], command: 'today' },
{ keywords: ['woche', 'week', 'wochenstatistik'], command: 'week' },
{ keywords: ['realtime', 'live', 'aktive', 'jetzt'], command: 'realtime' },
{ keywords: ['users', 'benutzer', 'nutzer', 'registrierte'], command: 'users' },
]);
constructor(
configService: ConfigService,
private analyticsService: AnalyticsService,
@ -33,6 +48,12 @@ export class MatrixService extends BaseMatrixService {
message: string,
_sender: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(message);
if (keywordCommand) {
message = `!${keywordCommand}`;
}
if (!message.startsWith('!')) return;
const [command] = message.slice(1).split(' ');

View file

@ -1,6 +1,13 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent, UserListMapper } from '@manacore/matrix-bot-common';
import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
UserListMapper,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { StorageService, StorageFile, Folder, ShareLink, TrashItem } from '../storage/storage.service';
import { SessionService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
@ -14,6 +21,17 @@ export class MatrixService extends BaseMatrixService {
private trashMapper = new UserListMapper<TrashItem>();
private currentFolder: Map<string, string | null> = new Map();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['dateien', 'files', 'meine dateien', 'liste'], command: 'dateien' },
{ keywords: ['ordner', 'folders', 'verzeichnisse', 'dirs'], command: 'ordner' },
{ keywords: ['teilen', 'share', 'freigeben', 'link erstellen'], command: 'teilen' },
{ keywords: ['suche', 'search', 'finde', 'durchsuchen'], command: 'suche' },
{ keywords: ['favoriten', 'favorites', 'favs', 'gemerkte'], command: 'favoriten' },
{ keywords: ['papierkorb', 'trash', 'geloeschte', 'muell'], command: 'papierkorb' },
{ keywords: ['links', 'shares', 'freigaben', 'geteilte'], command: 'links' },
]);
constructor(
configService: ConfigService,
private storageService: StorageService,
@ -36,6 +54,12 @@ export class MatrixService extends BaseMatrixService {
event: MatrixRoomEvent,
body: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(body);
if (keywordCommand) {
body = `!${keywordCommand}`;
}
if (!body.startsWith('!')) return;
const sender = event.sender;

View file

@ -4,6 +4,8 @@ import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { TtsService } from '../tts/tts.service';
import { HELP_TEXT, WELCOME_TEXT } from '../config/configuration';
@ -25,6 +27,13 @@ export class MatrixService extends BaseMatrixService {
// Track processed events to prevent duplicates
private processedEvents: Set<string> = new Set();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['voice', 'stimme', 'stimme aendern'], command: 'voice' },
{ keywords: ['voices', 'stimmen', 'verfuegbare stimmen'], command: 'voices' },
{ keywords: ['speed', 'geschwindigkeit', 'tempo'], command: 'speed' },
]);
constructor(
configService: ConfigService,
private ttsService: TtsService
@ -93,6 +102,12 @@ export class MatrixService extends BaseMatrixService {
const userId = event.sender;
try {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(body);
if (keywordCommand) {
body = `!${keywordCommand}`;
}
// Handle ! commands
if (body.startsWith('!')) {
const [command, ...args] = body.slice(1).split(' ');