diff --git a/Endpoints/ApiKeysEndpoint.cs b/Endpoints/ApiKeysEndpoint.cs index 6e8a9fe..0464834 100644 --- a/Endpoints/ApiKeysEndpoint.cs +++ b/Endpoints/ApiKeysEndpoint.cs @@ -540,8 +540,16 @@ public static class ApiKeysEndpoint return; } - await db.SetUserSelectedApiKey(userId, keyId); - await ctx.Response.WriteAsJsonAsync(new { ok = true, keyId, name = key.Name }); + var agentId = ctx.Request.Query["agentId"].FirstOrDefault(); + if (string.IsNullOrEmpty(agentId)) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsJsonAsync(new { error = "agentId required" }); + return; + } + + await db.SetAgentApiKey(userId, agentId, keyId); + await ctx.Response.WriteAsJsonAsync(new { ok = true, keyId, agentId, name = key.Name }); } private static async Task HandleGetSelected(HttpContext ctx, DatabaseService db) @@ -549,7 +557,14 @@ public static class ApiKeysEndpoint var userId = ctx.GetUserId(); if (userId == null) { ctx.Response.StatusCode = 401; return; } - var key = await db.GetUserSelectedApiKey(userId); + var agentId = ctx.Request.Query["agentId"].FirstOrDefault(); + if (string.IsNullOrEmpty(agentId)) + { + await ctx.Response.WriteAsJsonAsync(new { selected = false }); + return; + } + + var key = await db.GetAgentApiKeyRecord(userId, agentId); if (key == null) { await ctx.Response.WriteAsJsonAsync(new { selected = false }); diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index b51c058..a32fcc3 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -51,15 +51,6 @@ public static class ChatEndpoint var userId = req.HttpContext.GetUserId(); - // API-Key des Users auflösen - var userApiKey = await db.GetUserSelectedApiKey(userId); - if (userApiKey == null) - { - res.StatusCode = 400; - await res.WriteAsJsonAsync(new { error = "Kein API-Key ausgewählt. Bitte wähle einen Key aus." }); - return; - } - var maxIterations = settings.GetInt("MaxIterations", 100); var hasRemote = remoteMgr.GetBySessionId(sessionId) != null; @@ -71,6 +62,15 @@ public static class ChatEndpoint return; } + // API-Key des Users für diesen Agent auflösen + var userApiKey = await db.GetAgentApiKeyRecord(userId, ctx.AgentId); + if (userApiKey == null) + { + res.StatusCode = 400; + await res.WriteAsJsonAsync(new { error = "Kein API-Key für diesen Agent zugewiesen. Bitte weise einen Key zu." }); + return; + } + if (isChoiceResponse) { // Choice-Antwort: tool_result in die VORHERIGE Chain speichern (wo der tool_use liegt) diff --git a/Services/CronjobService.cs b/Services/CronjobService.cs index cb2008f..a405bb4 100644 --- a/Services/CronjobService.cs +++ b/Services/CronjobService.cs @@ -86,10 +86,10 @@ public class CronjobService : BackgroundService if (ctx == null) throw new Exception("Session oder Agent nicht gefunden / nicht konfiguriert"); - // API-Key des Cronjob-Owners auflösen - var userApiKey = await _db.GetUserSelectedApiKey(job.UserId); + // API-Key des Cronjob-Owners für diesen Agent auflösen + var userApiKey = await _db.GetAgentApiKeyRecord(job.UserId, ctx.AgentId); if (userApiKey == null) - throw new Exception($"Kein API-Key für User {job.UserId} ausgewählt"); + throw new Exception($"Kein API-Key für Agent {ctx.AgentId} zugewiesen (User {job.UserId})"); ctx.ApiToken = userApiKey.ApiKey; // History aus DB laden @@ -140,11 +140,11 @@ public class CronjobService : BackgroundService return; } - // API-Key des Cronjob-Owners auflösen - var notifyApiKey = await _db.GetUserSelectedApiKey(job.UserId); + // API-Key des Cronjob-Owners für Notify-Agent auflösen + var notifyApiKey = await _db.GetAgentApiKeyRecord(job.UserId, notifyCtx.AgentId); if (notifyApiKey == null) { - _logger.LogWarning("Kein API-Key für User {UserId} (Cronjob '{Name}' Notification)", job.UserId, job.Name); + _logger.LogWarning("Kein API-Key für Agent {AgentId} (Cronjob '{Name}' Notification)", notifyCtx.AgentId, job.Name); return; } notifyCtx.ApiToken = notifyApiKey.ApiKey; diff --git a/Services/DatabaseService.Integrations.cs b/Services/DatabaseService.Integrations.cs index 9f54dce..67c4a28 100644 --- a/Services/DatabaseService.Integrations.cs +++ b/Services/DatabaseService.Integrations.cs @@ -427,9 +427,9 @@ public partial class DatabaseService await conn.ExecuteAsync( "UPDATE scanned_keys SET adopted_api_key_id = NULL WHERE adopted_api_key_id = @keyId", new { keyId }); - // User die diesen Key ausgewählt haben zurücksetzen + // Agent-Key-Zuordnungen die auf diesen Key verweisen löschen await conn.ExecuteAsync( - "UPDATE users SET selected_api_key_id = NULL WHERE selected_api_key_id = @keyId", + "DELETE FROM agent_api_keys WHERE key_id = @keyId", new { keyId }); await conn.ExecuteAsync("DELETE FROM api_keys WHERE key_id = @keyId", new { keyId }); } diff --git a/Services/DatabaseService.Users.cs b/Services/DatabaseService.Users.cs index a49baa8..89dc027 100644 --- a/Services/DatabaseService.Users.cs +++ b/Services/DatabaseService.Users.cs @@ -187,32 +187,43 @@ public partial class DatabaseService await conn.ExecuteAsync("DELETE FROM users WHERE user_id = @userId", new { userId }); } - // ── User API-Key Auswahl ── + // ── Agent API-Key Zuordnung ── - public async Task SetUserSelectedApiKey(string userId, string keyId) + public async Task SetAgentApiKey(string userId, string agentId, string keyId) { using var conn = CreateConnection(); - await conn.ExecuteAsync( - "UPDATE users SET selected_api_key_id = @keyId, updated_at = @now WHERE user_id = @userId", - new { userId, keyId = (object)keyId ?? DBNull.Value, now = DateTime.UtcNow.ToString("o") }); + if (string.IsNullOrEmpty(keyId)) + { + await conn.ExecuteAsync( + "DELETE FROM agent_api_keys WHERE user_id = @userId AND agent_id = @agentId", + new { userId, agentId }); + } + else + { + await conn.ExecuteAsync(@" + INSERT INTO agent_api_keys (user_id, agent_id, key_id) + VALUES (@userId, @agentId, @keyId) + ON CONFLICT(user_id, agent_id) DO UPDATE SET key_id = @keyId", + new { userId, agentId, keyId }); + } } - public async Task GetUserSelectedApiKeyId(string userId) + public async Task GetAgentApiKeyId(string userId, string agentId) { using var conn = CreateConnection(); return await conn.QuerySingleOrDefaultAsync( - "SELECT selected_api_key_id FROM users WHERE user_id = @userId", - new { userId }); + "SELECT key_id FROM agent_api_keys WHERE user_id = @userId AND agent_id = @agentId", + new { userId, agentId }); } - public async Task GetUserSelectedApiKey(string userId) + public async Task GetAgentApiKeyRecord(string userId, string agentId) { using var conn = CreateConnection(); return await conn.QuerySingleOrDefaultAsync(@" SELECT k.* FROM api_keys k - INNER JOIN users u ON u.selected_api_key_id = k.key_id - WHERE u.user_id = @userId", - new { userId }); + INNER JOIN agent_api_keys a ON a.key_id = k.key_id + WHERE a.user_id = @userId AND a.agent_id = @agentId", + new { userId, agentId }); } /// diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 8b00f86..171460e 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -357,9 +357,23 @@ public partial class DatabaseService TryAddColumn(conn, "api_keys", "source_url", "TEXT"); TryAddColumn(conn, "api_keys", "is_public", "INTEGER NOT NULL DEFAULT 0"); - // users: ausgewählter API-Key + // users: ausgewählter API-Key (legacy, wird durch agent_api_keys ersetzt) TryAddColumn(conn, "users", "selected_api_key_id", "TEXT"); + conn.Execute(@" + CREATE TABLE IF NOT EXISTS agent_api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + agent_id TEXT NOT NULL, + key_id TEXT NOT NULL, + UNIQUE(user_id, agent_id) + ); + CREATE INDEX IF NOT EXISTS idx_agent_api_keys_user ON agent_api_keys(user_id); + "); + + // Migration: selected_api_key_id → agent_api_keys (einmalig) + MigrateSelectedApiKeysToAgentKeys(conn); + // Migration: Bestehende PNG/JPEG-Avatare/Images zu WebP konvertieren MigrateImagesToWebP(conn); @@ -367,6 +381,43 @@ public partial class DatabaseService MigrateImageThumbnails(conn); } + private void MigrateSelectedApiKeysToAgentKeys(SqliteConnection conn) + { + // Nur migrieren wenn agent_api_keys leer ist und users mit selected_api_key_id existieren + var count = conn.ExecuteScalar("SELECT COUNT(*) FROM agent_api_keys"); + if (count > 0) return; + + var usersWithKeys = conn.Query<(string UserId, string SelectedApiKeyId)>( + "SELECT user_id, selected_api_key_id FROM users WHERE selected_api_key_id IS NOT NULL AND selected_api_key_id != ''") + .ToList(); + + foreach (var (userId, keyId) in usersWithKeys) + { + // Alle Agents des Users + öffentliche Agents + var agentIds = conn.Query( + "SELECT agent_id FROM agents WHERE user_id = @userId OR is_public = 1", + new { userId }).ToList(); + + // Alle Sessions des Users mit Agent → deren Agents auch berücksichtigen + var sessionAgentIds = conn.Query( + "SELECT DISTINCT agent_id FROM sessions WHERE user_id = @userId AND agent_id IS NOT NULL AND agent_id != ''", + new { userId }).ToList(); + + var allAgentIds = agentIds.Union(sessionAgentIds).Distinct().ToList(); + + foreach (var agentId in allAgentIds) + { + try + { + conn.Execute( + "INSERT OR IGNORE INTO agent_api_keys (user_id, agent_id, key_id) VALUES (@userId, @agentId, @keyId)", + new { userId, agentId, keyId }); + } + catch { /* UNIQUE constraint – already exists */ } + } + } + } + private void MigrateImagesToWebP(SqliteConnection conn) { const int webPQuality = 80; diff --git a/Services/TelegramService.cs b/Services/TelegramService.cs index 3618c0a..a2c6b5b 100644 --- a/Services/TelegramService.cs +++ b/Services/TelegramService.cs @@ -168,11 +168,11 @@ public class TelegramService return; } - // API-Key des Bot-Owners auflösen - var userApiKey = await _db.GetUserSelectedApiKey(botUserId); + // API-Key des Bot-Owners für diesen Agent auflösen + var userApiKey = await _db.GetAgentApiKeyRecord(botUserId, ctx.AgentId); if (userApiKey == null) { - await SendMessage(chatId, botToken, "❌ Kein API-Key ausgewählt. Der Bot-Besitzer muss im Web-UI einen Key auswählen."); + await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen. Der Bot-Besitzer muss im Web-UI einen Key zuweisen."); return; } ctx.ApiToken = userApiKey.ApiKey; diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index 6c1a7b2..02293ed 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -224,8 +224,8 @@ function apiToggleApiKeyPublic(id) { return _put('/api/admin/api-keys/' + id + ' // ── User Key Selection API ── function apiGetAvailableKeys() { return _get('/api/api-keys/available', []); } -function apiSelectApiKey(keyId) { return _put('/api/api-keys/select/' + keyId, undefined, 'Fehler beim Auswählen'); } -function apiGetSelectedKey() { return _get('/api/api-keys/selected', null); } +function apiSelectApiKey(keyId, agentId) { return _put('/api/api-keys/select/' + keyId + '?agentId=' + encodeURIComponent(agentId), undefined, 'Fehler beim Auswählen'); } +function apiGetSelectedKey(agentId) { return _get('/api/api-keys/selected' + (agentId ? '?agentId=' + encodeURIComponent(agentId) : ''), null); } function apiDeleteApiKey(id) { return _del('/api/admin/api-keys/' + id, 'Fehler beim Löschen'); } function apiCheckApiKey(id) { return _post('/api/admin/api-keys/' + id + '/check', undefined, 'Fehler beim Prüfen'); } function apiCheckAllApiKeys() { return _post('/api/admin/api-keys/check-all', undefined, 'Fehler beim Prüfen'); } diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js index 08dacf5..1ce0224 100644 --- a/wwwroot/js/pages/chat-page.js +++ b/wwwroot/js/pages/chat-page.js @@ -49,7 +49,7 @@ async function _initChatUsageBadge() { if (!container) return; try { - var selectedKey = await apiGetSelectedKey(); + var selectedKey = await apiGetSelectedKey(getCurrentAgentId()); if (!selectedKey || !selectedKey.selected) { container.innerHTML = ''; var noKeyBtn = document.createElement('button'); @@ -85,7 +85,7 @@ async function _refreshChatUsageBadge() { var container = document.getElementById('chatUsageBadge'); if (!container || !_chatUsageKeyId) return; try { - var selectedKey = await apiGetSelectedKey(); + var selectedKey = await apiGetSelectedKey(getCurrentAgentId()); if (selectedKey) _renderChatUsageBadge(container, selectedKey); } catch(e) { /* ignore */ } } @@ -119,7 +119,7 @@ function _renderChatUsageBadge(container, key) { checkBtn.querySelector('i').className = 'fa fa-spinner fa-spin'; try { await apiCheckApiKey(_chatUsageKeyId); - var selectedKey = await apiGetSelectedKey(); + var selectedKey = await apiGetSelectedKey(getCurrentAgentId()); if (selectedKey) _renderChatUsageBadge(container, selectedKey); } catch(e) { console.error('Key check failed:', e); @@ -136,7 +136,7 @@ async function _showKeySwitchModal() { try { keys = await apiGetAvailableKeys(); } catch(e) { return; } var selectedKey = null; - try { selectedKey = await apiGetSelectedKey(); } catch(e) { /* ignore */ } + try { selectedKey = await apiGetSelectedKey(getCurrentAgentId()); } catch(e) { /* ignore */ } var selectedKeyId = selectedKey ? selectedKey.keyId : null; var { body, close } = _createModal('API Key wechseln', { modal: 'key-switch-modal' }); @@ -187,7 +187,7 @@ async function _showKeySwitchModal() { row.addEventListener('click', async function() { row.classList.add('loading-state'); try { - await apiSelectApiKey(key.keyId); + await apiSelectApiKey(key.keyId, getCurrentAgentId()); close(); await _initChatUsageBadge(); } catch(e) { @@ -352,6 +352,8 @@ registerPage('chat', { return; } + currentAgentId = session.agentId; + const titleEl = document.getElementById('sessionTitle'); if (titleEl) { titleEl.textContent = session.name || 'Unbenannt'; @@ -605,6 +607,7 @@ registerPage('chat', { _chatAbandoned = true; stopTypingIndicator(); setCurrentAgentName(null); + currentAgentId = null; closeEmojiPicker(); // Usage Badge cleanup if (_chatUsageScrollHandler) { diff --git a/wwwroot/js/session.js b/wwwroot/js/session.js index 601f976..3209edb 100644 --- a/wwwroot/js/session.js +++ b/wwwroot/js/session.js @@ -1,7 +1,12 @@ // ── Session State ── let currentSessionId = null; +let currentAgentId = null; function getCurrentSessionId() { return currentSessionId; } + +function getCurrentAgentId() { + return currentAgentId; +}