diff --git a/src/lib/utils/connections.ts b/src/lib/utils/connections.ts new file mode 100644 index 0000000000..7c7bc0408d --- /dev/null +++ b/src/lib/utils/connections.ts @@ -0,0 +1,124 @@ +/** + * Shared helpers for managing system-level connections. + * Used by both the admin settings UI and the desktop event handler + * to ensure consistent add/remove logic. + */ + +import { getOpenAIConfig, updateOpenAIConfig } from '$lib/apis/openai'; +import { + getTerminalServerConnections, + setTerminalServerConnections +} from '$lib/apis/configs'; + +// ─── OpenAI Connections ───────────────────────────────── + +/** + * Add an OpenAI-compatible API connection at the system level. + * Mirrors the logic in admin/Settings/Connections.svelte. + */ +export const addOpenAIConnection = async ( + token: string, + connection: { url: string; key?: string; config?: object } +) => { + const current = await getOpenAIConfig(token); + const urls = current?.OPENAI_API_BASE_URLS ?? []; + const keys = current?.OPENAI_API_KEYS ?? []; + const configs = current?.OPENAI_API_CONFIGS ?? {}; + + const normalizedUrl = connection.url.replace(/\/$/, ''); + + // Don't add duplicates + if (urls.map((u: string) => u.replace(/\/$/, '')).includes(normalizedUrl)) { + return current; + } + + urls.push(normalizedUrl); + keys.push(connection.key ?? ''); + if (connection.config) { + configs[(urls.length - 1).toString()] = connection.config; + } + + return await updateOpenAIConfig(token, { + ENABLE_OPENAI_API: current?.ENABLE_OPENAI_API ?? true, + OPENAI_API_BASE_URLS: urls, + OPENAI_API_KEYS: keys, + OPENAI_API_CONFIGS: configs + }); +}; + +/** + * Remove an OpenAI-compatible API connection by URL at the system level. + * Re-indexes OPENAI_API_CONFIGS to match the admin delete pattern. + */ +export const removeOpenAIConnection = async (token: string, url: string) => { + const current = await getOpenAIConfig(token); + const urls: string[] = current?.OPENAI_API_BASE_URLS ?? []; + const keys: string[] = current?.OPENAI_API_KEYS ?? []; + const configs: Record = current?.OPENAI_API_CONFIGS ?? {}; + + const normalizedUrl = url.replace(/\/$/, ''); + const idx = urls.findIndex((u: string) => u.replace(/\/$/, '') === normalizedUrl); + if (idx === -1) return current; + + const newUrls = urls.filter((_: string, i: number) => i !== idx); + const newKeys = keys.filter((_: string, i: number) => i !== idx); + + // Re-index configs (mirrors admin/Settings/Connections.svelte onDelete) + const newConfigs: Record = {}; + newUrls.forEach((_: string, newIdx: number) => { + newConfigs[newIdx] = configs[newIdx < idx ? newIdx : newIdx + 1]; + }); + + return await updateOpenAIConfig(token, { + ENABLE_OPENAI_API: current?.ENABLE_OPENAI_API ?? true, + OPENAI_API_BASE_URLS: newUrls, + OPENAI_API_KEYS: newKeys, + OPENAI_API_CONFIGS: newConfigs + }); +}; + +// ─── Terminal Server Connections ──────────────────────── + +/** + * Add a terminal server connection at the system level. + * Mirrors the logic in admin/Settings/Integrations.svelte. + */ +export const addTerminalConnection = async ( + token: string, + connection: { url: string; key?: string; name?: string; auth_type?: string } +) => { + const current = await getTerminalServerConnections(token); + const servers = current?.TERMINAL_SERVER_CONNECTIONS ?? []; + + // Don't add duplicates + if (servers.find((s: any) => s.url === connection.url)) { + return current; + } + + servers.push({ + url: connection.url, + key: connection.key ?? '', + auth_type: connection.auth_type ?? 'bearer', + name: connection.name ?? 'Open Terminal', + enabled: true + }); + + return await setTerminalServerConnections(token, { + TERMINAL_SERVER_CONNECTIONS: servers + }); +}; + +/** + * Remove a terminal server connection by URL at the system level. + */ +export const removeTerminalConnection = async (token: string, url: string) => { + const current = await getTerminalServerConnections(token); + const servers = current?.TERMINAL_SERVER_CONNECTIONS ?? []; + + const filtered = servers.filter((s: any) => s.url !== url); + if (filtered.length === servers.length) return current; // nothing to remove + + return await setTerminalServerConnections(token, { + TERMINAL_SERVER_CONNECTIONS: filtered + }); +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 36fd7b22d5..12962cbcbb 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -53,6 +53,12 @@ import { getSessionUser, userSignOut } from '$lib/apis/auths'; import { getAllTags, getChatList } from '$lib/apis/chats'; import { chatCompletion } from '$lib/apis/openai'; + import { + addOpenAIConnection, + removeOpenAIConnection, + addTerminalConnection, + removeTerminalConnection + } from '$lib/utils/connections'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL, WEBUI_HOSTNAME } from '$lib/constants'; import { bestMatchingLanguage, displayFileHandler } from '$lib/utils'; @@ -692,6 +698,45 @@ } }; + const desktopEventHandler = async (event) => { + // Events that don't require auth + if (event.type === 'page:reload') { + location.reload(); + return; + } + + const token = localStorage.token; + if (!token) return; + + // Only admins can modify system-level connections + if ($user?.role !== 'admin') return; + + try { + if (event.type === 'connections:terminal') { + if (event.data.action === 'add') { + await addTerminalConnection(token, { + url: event.data.url, + key: event.data.key, + name: 'Local Open Terminal' + }); + } else if (event.data.action === 'remove') { + await removeTerminalConnection(token, event.data.url); + } + } else if (event.type === 'connections:openai') { + if (event.data.action === 'add') { + await addOpenAIConnection(token, { + url: event.data.url, + key: event.data.key + }); + } else if (event.data.action === 'remove') { + await removeOpenAIConnection(token, event.data.url); + } + } + } catch (e) { + console.error('Desktop connection update failed:', e); + } + }; + const windowMessageEventHandler = async (event) => { if ( !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:9999'].includes( @@ -767,6 +812,11 @@ appData.set(data); } } + + // Listen for desktop service lifecycle events (scalable protocol) + if (window.electronAPI.onEvent) { + window.electronAPI.onEvent(desktopEventHandler); + } } // Listen for messages on the BroadcastChannel