Add KI/LLM integration with OpenAI-compatible API support
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 30s

Features:
- /ai setup - Setup wizard via private chat for security
- /ai status - Show current config (API key masked)
- /ai on/off - Enable/disable AI per chat
- /ai random <1-10> - Set random trigger probability (admins)
- /ai help - Detailed help
- /ask-ai [text] - Generate AI response

Security:
- API keys only entered in private chat
- API keys masked in status display
- Admin-only configuration

Architecture:
- OpenAI-compatible client (OpenAI, Ollama, OpenRouter)
- Global system prompt (.env) + group prompt (DB)
- 50 message context per chat
- SQLite tables: ai_settings, message_context, setup_sessions

Defaults:
- AI disabled by default
- Trigger word: ask-ai
- Random probability: 1%

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 10:40:51 +01:00
parent 42b2971cd8
commit 3b310d612d
6 changed files with 844 additions and 64 deletions

106
src/ai.ts Normal file
View File

@@ -0,0 +1,106 @@
/**
* OpenAI-compatible AI client for multiple providers.
* Supports OpenAI, Ollama, OpenRouter, and any OpenAI-compatible API.
*/
export interface AIConfig {
apiKey: string;
baseUrl: string;
model: string;
systemPrompt: string;
groupPrompt?: string;
}
export interface AIMessage {
role: 'user' | 'assistant' | 'system';
content: string;
}
/**
* Mask an API key for safe display
* sk-proj-abc123xyz... → sk-***...***xyz
*/
export function maskApiKey(key: string): string {
if (!key || key.length < 8) return '***';
const start = key.substring(0, 3);
const end = key.substring(key.length - 3);
return `${start}***...***${end}`;
}
/**
* Generate AI response using OpenAI-compatible API
*/
export async function generateAIResponse(
config: AIConfig,
context: AIMessage[],
userMessage: string
): Promise<string> {
const messages: AIMessage[] = [];
// Build system prompt (global + group)
let systemPrompt = config.systemPrompt;
if (config.groupPrompt) {
systemPrompt += '\n\n' + config.groupPrompt;
}
messages.push({ role: 'system', content: systemPrompt });
// Add context messages (last 50)
messages.push(...context.slice(-50));
// Add user message
messages.push({ role: 'user', content: userMessage });
const response = await fetch(`${config.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
model: config.model,
messages,
max_tokens: 500,
temperature: 0.7,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`AI API error: ${response.status} - ${error}`);
}
const data = await response.json() as { choices: Array<{ message?: { content?: string } }> };
return data.choices[0]?.message?.content || 'Keine Antwort erhalten.';
}
/**
* Get default base URL for a provider
*/
export function getDefaultBaseUrl(provider: string): string {
switch (provider) {
case 'openai':
return 'https://api.openai.com/v1';
case 'ollama':
return 'http://localhost:11434/v1';
case 'openrouter':
return 'https://openrouter.ai/api/v1';
default:
return 'https://api.openai.com/v1';
}
}
/**
* Get available models for a provider
*/
export function getProviderModels(provider: string): string[] {
switch (provider) {
case 'openai':
return ['gpt-4o-mini', 'gpt-4o', 'gpt-3.5-turbo'];
case 'ollama':
return ['llama3', 'llama3.1', 'mistral', 'codellama'];
case 'openrouter':
return ['openai/gpt-4o-mini', 'anthropic/claude-3.5-sonnet', 'meta-llama/llama-3.1-8b-instruct'];
default:
return ['gpt-4o-mini'];
}
}