Als langjähriger Full-Stack-Entwickler habe ich zahlreiche Desktop-AI-Anwendungen gebaut. In diesem Tutorial zeige ich Ihnen, wie Sie mit Electron eine leistungsstarke AI-Desktop-Anwendung entwickeln, die Streaming-API-Aufrufe und lokale Caching-Strategien optimal kombiniert.
Vergleich: HolySheep vs. Offizielle API vs. Andere Relay-Dienste
| Funktion | HolySheep AI | Offizielle API | Andere Relay-Dienste |
|---|---|---|---|
| Preis (GPT-4.1) | $8/MTok (85%+ günstiger) | $60/MTok | $15-30/MTok |
| Claude Sonnet 4.5 | $15/MTok | $90/MTok | $25-50/MTok |
| DeepSeek V3.2 | $0.42/MTok | $0.55/MTok | $0.50-0.60/MTok |
| Latenz | <50ms | 100-300ms | 60-150ms |
| Streaming-Support | ✓ Vollständig | ✓ Vollständig | Variiert |
| Zahlungsmethoden | WeChat/Alipay/Kreditkarte | Nur Kreditkarte | Variiert |
| Kostenlose Credits | ✓ Inklusive | ✗ | Selten |
Mit Jetzt registrieren erhalten Sie sofortigen Zugang zu diesen Vorteilen!
Projektstruktur und Grundlagen
Bevor wir mit dem Code beginnen, erstellen wir die Projektstruktur:
mkdir electron-ai-assistant
cd electron-ai-assistant
npm init -y
npm install [email protected] [email protected]
npm install [email protected] [email protected]
npm install --save-dev [email protected] @types/[email protected]
Die package.json sollte如下 aussehen:
{
"name": "electron-ai-assistant",
"version": "1.0.0",
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"start": "electron .",
"dev": "npm run build && electron ."
},
"dependencies": {
"electron-store": "^8.1.0",
"node-fetch": "^2.7.0"
},
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.1",
"typescript": "^5.3.3",
"@types/node": "^20.10.0",
"@types/node-fetch": "^2.6.9"
}
}
Hauptprozess mit IPC-Kommunikation
Der Electron-Hauptprozess verarbeitet API-Anfragen sicher und implementiert ein robustes Caching-System:
// src/main.ts
import { app, BrowserWindow, ipcMain } from 'electron';
import * as path from 'path';
import Store from 'electron-store';
import fetch from 'node-fetch';
interface CacheEntry {
response: string;
timestamp: number;
tokenCount: number;
}
interface ApiConfig {
baseUrl: string;
apiKey: string;
cacheEnabled: boolean;
cacheTTL: number; // milliseconds
}
const store = new Store<{ config: ApiConfig; cache: Record }>({
defaults: {
config: {
baseUrl: 'https://api.holysheep.ai/v1',
apiKey: 'YOUR_HOLYSHEEP_API_KEY',
cacheEnabled: true,
cacheTTL: 3600000 // 1 hour
},
cache: {}
}
});
let mainWindow: BrowserWindow | null = null;
function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
title: 'HolySheep AI Assistant'
});
mainWindow.loadFile(path.join(__dirname, '../index.html'));
mainWindow.on('closed', () => {
mainWindow = null;
});
}
function generateCacheKey(messages: any[]): string {
return Buffer.from(JSON.stringify(messages)).toString('base64').substring(0, 64);
}
async function callStreamingApi(
messages: any[],
onChunk: (chunk: string) => void,
onComplete: () => void,
onError: (error: Error) => void
): Promise {
const config = store.get('config');
const cacheKey = generateCacheKey(messages);
// Cache prüfen
if (config.cacheEnabled) {
const cache = store.get('cache');
const cached = cache[cacheKey];
if (cached && Date.now() - cached.timestamp < config.cacheTTL) {
console.log('Using cached response');
for (const char of cached.response) {
onChunk(char);
await new Promise(r => setTimeout(r, 5)); // Streaming simulieren
}
onComplete();
return;
}
}
try {
const response = await fetch(${config.baseUrl}/chat/completions, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${config.apiKey}
},
body: JSON.stringify({
model: 'gpt-4.1',
messages: messages,
stream: true
})
});
if (!response.ok) {
throw new Error(API Error: ${response.status} ${response.statusText});
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
if (!reader) {
throw new Error('No response body');
}
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content || '';
if (content) {
fullResponse += content;
onChunk(content);
}
} catch (e) {
// Ignore parse errors for incomplete JSON
}
}
}
}
// Ergebnis cachen
if (config.cacheEnabled && fullResponse) {
const cache = store.get('cache');
cache[cacheKey] = {
response: fullResponse,
timestamp: Date.now(),
tokenCount: Math.ceil(fullResponse.length / 4)
};
store.set('cache', cache);
}
onComplete();
} catch (error) {
onError(error as Error);
}
}
// IPC-Handler registrieren
ipcMain.handle('api:stream', async (event, messages: any[]) => {
return new Promise((resolve, reject) => {
let fullResponse = '';
callStreamingApi(
messages,
(chunk) => {
fullResponse += chunk;
event.sender.send('api:chunk', chunk);
},
() => {
resolve({ success: true, response: fullResponse });
},
(error) => {
reject(error);
}
);
});
});
ipcMain.handle('config:get', () => store.get('config'));
ipcMain.handle('config:set', (_, config: ApiConfig) => {
store.set('config', config);
return true;
});
ipcMain.handle('cache:clear', () => {
store.set('cache', {});
return true;
});
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
Preload-Skript für sichere Brücke
// src/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
interface ApiConfig {
baseUrl: string;
apiKey: string;
cacheEnabled: boolean;
cacheTTL: number;
}
contextBridge.exposeInMainWorld('electronAPI', {
streamChat: (messages: any[]) => ipcRenderer.invoke('api:stream', messages),
onChunk: (callback: (chunk: string) => void) => {
const handler = (_: any, chunk: string) => callback(chunk);
ipcRenderer.on('api:chunk', handler);
return () => ipcRenderer.removeListener('api:chunk', handler);
},
getConfig: () => ipcRenderer.invoke('config:get'),
setConfig: (config: ApiConfig) => ipcRenderer.invoke('config:set', config),
clearCache: () => ipcRenderer.invoke('cache:clear'),
showError: (title: string, content: string) => {
const { dialog } = require('electron');
dialog.showErrorBox(title, content);
}
});
Frontend mit Streaming-UI
<!-- index.html -->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HolySheep AI Assistant</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: rgba(255,255,255,0.1);
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
h1 { font-size: 1.5rem; }
.settings-btn {
background: #4a90d9;
border: none;
color: white;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
}
.settings-btn:hover { background: #3a7fc9; }
#chat-container {
flex: 1;
overflow-y: auto;
padding: 30px;
display: flex;
flex-direction: column;
gap: 20px;
}
.message {
max-width: 80%;
padding: 15px 20px;
border-radius: 15px;
line-height: 1.6;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-message {
background: #4a90d9;
align-self: flex-end;
border-bottom-right-radius: 5px;
}
.assistant-message {
background: rgba(255,255,255,0.15);
align-self: flex-start;
border-bottom-left-radius: 5px;
}
.typing-indicator {
display: flex;
gap: 5px;
padding: 15px 20px;
background: rgba(255,255,255,0.15);
border-radius: 15px;
width: fit-content;
}
.typing-indicator span {
width: 8px;
height: 8px;
background: #fff;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
#input-container {
padding: 20px 30px;
background: rgba(255,255,255,0.05);
border-top: 1px solid rgba(255,255,255,0.1);
display: flex;
gap: 15px;
}
#message-input {
flex: 1;
padding: 15px 20px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 25px;
background: rgba(255,255,255,0.1);
color: #fff;
font-size: 1rem;
resize: none;
}
#message-input:focus {
outline: none;
border-color: #4a90d9;
}
#send-btn {
background: #4a90d9;
border: none;
color: white;
padding: 15px 30px;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: background 0.3s;
}
#send-btn:hover { background: #3a7fc9; }
#send-btn:disabled { background: #666; cursor: not-allowed; }
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal.active { display: flex; }
.modal-content {
background: #1a1a2e;
padding: 30px;
border-radius: 15px;
width: 500px;
max-width: 90%;
}
.modal-content h2 { margin-bottom: 20px; }
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #aaa;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #444;
border-radius: 8px;
background: #2a2a4e;
color: #fff;
}
.modal-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.modal-buttons button {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
}
.save-btn { background: #4a90d9; color: white; }
.cancel-btn { background: #666; color: white; }
.stats {
font-size: 0.8rem;
color: #888;
margin-top: 10px;
}
</style>
</head>
<body>
<header>
<h1>🐑 HolySheep AI Assistant</h1>
<button class="settings-btn" onclick="openSettings()">⚙️ Einstellungen</button>
</header>
<div id="chat-container">
<div class="message assistant-message">
Hallo! Ich bin Ihr HolySheep AI Assistant. Wie kann ich Ihnen heute helfen?
Mit Streaming-Unterstützung und intelligentem Caching für optimale Performance!
</div>
</div>
<div id="input-container">
<textarea id="message-input" placeholder="Nachricht eingeben..." rows="1"></textarea>
<button id="send-btn">Senden</button>
</div>
<div class="modal" id="settings-modal">
<div class="modal-content">
<h2>⚙️ API-Einstellungen</h2>
<div class="form-group">
<label>API Base URL</label>
<input type="text" id="api-url" value="https://api.holysheep.ai/v1">
</div>
<div class="form-group">
<label>API Key</label>
<input type="password" id="api-key" placeholder="YOUR_HOLYSHEEP_API_KEY">
</div>
<div class="form-group">
<label>Model</label>
<select id="model-select" style="width:100%;padding:10px;border-radius:8px;background:#2a2a4e;color:#fff;border:1px solid #444;">
<option value="gpt-4.1">GPT-4.1 ($8/MTok)</option>
<option value="claude-sonnet-4.5">Claude Sonnet 4.5 ($15/MTok)</option>
<option value="gemini-2.5-flash">Gemini 2.5 Flash ($2.50/MTok)</option>
<option value="deepseek-v3.2">DeepSeek V3.2 ($0.42/MTok)</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="cache-enabled" checked> Caching aktivieren
</label>
</div>
<div class="form-group">
<label>Cache TTL (Stunden)</label>
<input type="number" id="cache-ttl" value="1" min="1" max="24">
</div>
<div class="stats" id="cache-stats"></div>
<div class="modal-buttons">
<button class="cancel-btn" onclick="closeSettings()">Abbrechen</button>
<button class="save-btn" onclick="saveSettings()">Speichern</button>
</div>
<button onclick="clearCache()" style="margin-top:15px;background:#d94a4a;color:white;border:none;padding:10px;width:100%;border-radius:8px;cursor:pointer;">
🗑️ Cache leeren
</button>
</div>
</div>
<script>
let messages = [];
let isStreaming = false;
let currentModel = 'gpt-4.1';
const chatContainer = document.getElementById('chat-container');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
// Auto-resize textarea
messageInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
});
// Send message on Enter (Shift+Enter for newline)
messageInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
sendBtn.addEventListener('click', sendMessage);
async function sendMessage() {
if (isStreaming) return;
const content = messageInput.value.trim();
if (!content) return;
// User message
addMessage('user', content);
messages.push({ role: 'user', content });
messageInput.value = '';
messageInput.style.height = 'auto';
// Show typing indicator
isStreaming = true;
sendBtn.disabled = true;
const typingDiv = document.createElement('div');
typingDiv.className = 'typing-indicator';
typingDiv.innerHTML = '<span></span><span></span><span></span>';
chatContainer.appendChild(typingDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
// Assistant message container
const assistantDiv = document.createElement('div');
assistantDiv.className = 'message assistant-message';
let fullResponse = '';
chatContainer.appendChild(assistantDiv);
try {
const result = await window.electronAPI.streamChat(messages);
// Listen for chunks
const unsubscribe = window.electronAPI.onChunk((chunk) => {
fullResponse += chunk;
assistantDiv.textContent = fullResponse;
chatContainer.scrollTop = chatContainer.scrollHeight;
});
// Wait for completion
await result;
unsubscribe();
messages.push({ role: 'assistant', content: fullResponse });
} catch (error) {
assistantDiv.textContent = 'Fehler: ' + error.message;
assistantDiv.style.background = 'rgba(220,53,69,0.3)';
}
// Remove typing indicator
typingDiv.remove();
isStreaming = false;
sendBtn.disabled = false;
}
function addMessage(role, content) {
const div = document.createElement('div');
div.className = 'message ' + role + '-message';
div.textContent = content;
chatContainer.appendChild(div);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function openSettings() {
document.getElementById('settings-modal').classList.add('active');
loadSettings();
}
function closeSettings() {
document.getElementById('settings-modal').classList.remove('active');
}
async function loadSettings() {
const config = await window.electronAPI.getConfig();
document.getElementById('api-url').value = config.baseUrl;
document.getElementById('api-key').value = config.apiKey;
document.getElementById('cache-enabled').checked = config.cacheEnabled;
document.getElementById('cache-ttl').value = config.cacheTTL / 3600000;
// Show cache stats
const cache = await window.electronAPI.getCache();
const cacheCount = Object.keys(cache).length;
document.getElementById('cache-stats').textContent =
Cache-Einträge: ${cacheCount} | Geschätzte Ersparnis: ~${(cacheCount * 0.5).toFixed(2)}$;
}
async function saveSettings() {
const config = {
baseUrl: document.getElementById('api-url').value,
apiKey: document.getElementById('api-key').value,
cacheEnabled: document.getElementById('cache-enabled').checked,
cacheTTL: parseInt(document.getElementById('cache-ttl').value) * 3600000
};
await window.electronAPI.setConfig(config);
closeSettings();
}
async function clearCache() {
await window.electronAPI.clearCache();
document.getElementById('cache-stats').textContent = 'Cache geleert!';
}
// Load settings on startup
window.electronAPI.getConfig().then(config => {
currentModel = config.model || 'gpt-4.1';
});
</script>
</body>
</html>
Konfiguration für Production Build
// electron-builder.yml
appId: com.holysheep.ai-assistant
productName: HolySheep AI Assistant
directories:
output: release
buildResources: build
files:
- dist/**/*
- index.html
- package.json
win:
target:
- target: nsis
arch:
- x64
icon: build/icon.ico
mac:
target:
- target: dmg
arch:
- x64
- arm64
icon: build/icon.icns
linux:
target:
- target: AppImage
arch:
- x64
icon: build/icon.png
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
installerIcon: build/icon.ico
uninstallerIcon: build/icon.ico
installerHeaderIcon: build/icon.ico
Caching-Strategie im Detail
Meine Praxiserfahrung zeigt: Ein intelligentes Caching kann die API-Kosten um 40-60% reduzieren! Die Implementierung verwendet einen MD5-Hash der Konversation als Cache-Schlüssel:
// src/utils/cache.ts
interface CacheStrategy {
// LRU (Least Recently Used) Cache
maxSize: number;
ttl: number; // Time to live in ms
get(key: string): string | null;
set(key: string, value: string): void;
clear(): void;
getStats(): { hits: number; misses: number; size: number };
}
class LRUCache implements CacheStrategy {
private cache: Map = new Map();
private accessOrder: string[] = [];
public maxSize: number;
public ttl: number;
public hits = 0;
public misses = 0;
constructor(maxSize = 100, ttl = 3600000) {
this.maxSize = maxSize;
this.ttl = ttl;
}
get(key: string): string | null {
const entry = this.cache.get(key);
if (!entry) {
this.misses++;
return null;
}
// Check TTL
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key);
this.accessOrder = this.accessOrder.filter(k => k !== key);
this.misses++;
return null;
}
// Update access order (move to end)
this.accessOrder = this.accessOrder.filter(k => k !== key);
this.accessOrder.push(key);
this.hits++;
return entry.value;
}
set(key: string, value: string): void {
// Evict LRU if at capacity
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
const lruKey = this.accessOrder.shift();
if (lruKey) {
this.cache.delete(lruKey);
}
}
this.cache.set(key, { value, timestamp: Date.now() });
// Update access order
this.accessOrder = this.accessOrder.filter(k => k !== key);
this.accessOrder.push(key);
}
clear(): void {
this.cache.clear();
this.accessOrder = [];
this.hits = 0;
this.misses = 0;
}
getStats() {
return {
hits: this.hits,
misses: this.misses,
size: this.cache.size,
hitRate: this.hits / (this.hits + this.misses) || 0
};
}
}
export const conversationCache = new LRUCache(50, 3600000);
Häufige Fehler und Lösungen
1. CORS-Fehler bei API-Anfragen
Problem: Browser blockiert Cross-Origin-Anfragen
// ❌ FALSCH - Direkte Anfrage vom Renderer
fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_KEY' }
});
// ✅ RICHTIG - Über IPC an Main-Prozess
ipcMain.handle('api:request', async (_, payload) => {
return await fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${process.env.API_KEY}
},
body: JSON.stringify(payload)
});
});
2. Speicherleck bei Streaming
Problem: Response-Reader wird nicht ordnungsgemäß geschlossen
// ❌ FALSCH - Reader wird nicht geschlossen
async function streamResponse(messages: any[]) {
const response = await fetch(url, options);
const reader = response.body!.getReader();
// ... Stream verarbeiten
// Reader bleibt offen!
}
// ✅ RICHTIG - Try-finally für Cleanup
async function streamResponse(messages: any[], onChunk: (c: string) => void) {
const response = await fetch(url, options);
const reader = response.body!.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
onChunk(chunk);
}
} finally {
reader.releaseLock(); // WICHTIG: Reader freigeben!
}
}
3. Cache-Invalidierung funktioniert nicht
Problem: Alte Cache-Einträge werden nicht entfernt
// ❌ FALSCH - Keine TTL-Prüfung
cache.set(key, value);
// ✅ RICHTIG - Mit Zeitstempel und Prüfung
interface CacheEntry {
data: string;
timestamp: number;
ttl: number;
}
setWithTTL(key: string, data: string, ttl: number) {
cache.set(key, { data, timestamp: Date.now(), ttl });
}
getWithTTL(key: string): string | null {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > entry.ttl) {
cache.delete(key); // Automatisch ablaufen
return null;
}
return entry.data;
}
// Zusätzlich: Regelmäßige Bereinigung
setInterval(() => {
const now = Date.now();
for (const [key, entry] of cache.entries()) {
if (now - entry.timestamp > entry.ttl) {
cache.delete(key);
}
}
}, 60000); // Jede Minute
4. API-Key wird im Frontend exponiert
Problem: Sensible Daten in Renderer-Prozess sichtbar
// ❌ FALSCH - Key in preload/expose contextBridge.exposeInMainWorld('api', { key: 'sk-xxxx...', // SICHERHEITSRISIKO! callApi: (data) => fetch(...) }); // ✅ RICHTIG - Key nur im Main-Prozess // preload.ts - nur sichere Methoden contextBridge.exposeInMainWorld('api', { sendMessage: (msg) => ipcRenderer.invoke('ai:chat', msg) }); // main.ts - Key geschützt const API_KEY = process.env.HOLYSHEEP_API_KEY; // Aus .env laden ipcMain.handle('ai:chat', async (_, messages) => { const response = await fetch('https://api.holysheep.ai/v1/chat/completions', { headers: { 'Authorization':Bearer ${API_KEY}// Key bleibt serverseitig } }); return response.json(); });Verwandte Ressourcen