/** * TASKMATE - Claude Assistant Routes * ==================================== * REST-Endpunkte und Session-Manager fuer den Claude-Assistenten */ const express = require('express'); const router = express.Router(); const { spawn } = require('child_process'); const { getDb } = require('../database'); const logger = require('../utils/logger'); // ============================================================================ // SESSION MANAGER // ============================================================================ // Aktive Prozesse: userId -> { process, sessionId, timeout, outputBuffer, saveTimer } const activeSessions = new Map(); const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 Minuten const DB_SAVE_INTERVAL_MS = 2000; // Output alle 2 Sekunden in DB speichern /** * Berechtigungs-Middleware: Nur Hendrik und Monami duerfen den Assistenten nutzen */ function requireAssistantAccess(req, res, next) { const username = (req.user.username || '').toLowerCase(); if (username !== 'hendrik' && username !== 'monami') { return res.status(403).json({ error: 'Kein Zugriff auf den Assistenten' }); } next(); } /** * Socket-Berechtigung pruefen */ function checkSocketAccess(socket) { const username = (socket.user.username || '').toLowerCase(); return username === 'hendrik' || username === 'monami'; } /** * Timeout zuruecksetzen fuer eine Session */ function resetTimeout(userId) { const session = activeSessions.get(userId); if (!session) return; if (session.timeout) { clearTimeout(session.timeout); } session.timeout = setTimeout(() => { logger.info(`[Assistant] Session-Timeout fuer User ${userId} - Prozess wird beendet`); stopSession(userId); }, SESSION_TIMEOUT_MS); } /** * Gepufferten Output in DB speichern (Debounce) */ function flushOutputBuffer(userId) { const session = activeSessions.get(userId); if (!session || !session.outputBuffer || session.outputBuffer.length === 0) return; try { const db = getDb(); const content = session.outputBuffer.join(''); session.outputBuffer = []; db.prepare(` INSERT INTO assistant_messages (session_id, role, content) VALUES (?, 'assistant', ?) `).run(session.sessionId, content); } catch (err) { logger.error(`[Assistant] Fehler beim Speichern des Outputs: ${err.message}`); } } /** * Claude-Prozess starten */ function startSession(userId, sessionId, socket) { if (activeSessions.has(userId)) { throw new Error('Es laeuft bereits eine aktive Session'); } const proc = spawn('claude', [], { cwd: '/home/claude-dev/TaskMate', env: { ...process.env, HOME: '/home/claude-dev' }, stdio: ['pipe', 'pipe', 'pipe'] }); const sessionData = { process: proc, sessionId, timeout: null, outputBuffer: [], saveTimer: null }; activeSessions.set(userId, sessionData); resetTimeout(userId); logger.info(`[Assistant] Session ${sessionId} gestartet fuer User ${userId} (PID: ${proc.pid})`); // stdout verarbeiten proc.stdout.on('data', (data) => { const text = data.toString(); sessionData.outputBuffer.push(text); // An Socket senden if (socket && socket.connected) { socket.emit('assistant:output', { sessionId, content: text, stream: 'stdout' }); } // Debounced DB-Save if (!sessionData.saveTimer) { sessionData.saveTimer = setTimeout(() => { sessionData.saveTimer = null; flushOutputBuffer(userId); }, DB_SAVE_INTERVAL_MS); } }); // stderr verarbeiten (gleich wie stdout) proc.stderr.on('data', (data) => { const text = data.toString(); sessionData.outputBuffer.push(text); if (socket && socket.connected) { socket.emit('assistant:output', { sessionId, content: text, stream: 'stderr' }); } if (!sessionData.saveTimer) { sessionData.saveTimer = setTimeout(() => { sessionData.saveTimer = null; flushOutputBuffer(userId); }, DB_SAVE_INTERVAL_MS); } }); // Prozess beendet proc.on('close', (code) => { logger.info(`[Assistant] Prozess beendet fuer User ${userId} (Code: ${code})`); // Restlichen Buffer speichern flushOutputBuffer(userId); // Aufraumen const session = activeSessions.get(userId); if (session) { if (session.timeout) clearTimeout(session.timeout); if (session.saveTimer) clearTimeout(session.saveTimer); activeSessions.delete(userId); } // Session in DB als beendet markieren try { const db = getDb(); db.prepare(` UPDATE assistant_sessions SET status = 'ended', ended_at = CURRENT_TIMESTAMP WHERE id = ? `).run(sessionId); } catch (err) { logger.error(`[Assistant] Fehler beim Beenden der Session: ${err.message}`); } // Socket benachrichtigen if (socket && socket.connected) { socket.emit('assistant:status', { sessionId, status: 'ended', code }); } }); proc.on('error', (err) => { logger.error(`[Assistant] Prozess-Fehler fuer User ${userId}: ${err.message}`); const session = activeSessions.get(userId); if (session) { if (session.timeout) clearTimeout(session.timeout); if (session.saveTimer) clearTimeout(session.saveTimer); activeSessions.delete(userId); } if (socket && socket.connected) { socket.emit('assistant:status', { sessionId, status: 'error', error: err.message }); } }); return proc; } /** * Nachricht an Claude-Prozess senden */ function sendMessage(userId, message) { const session = activeSessions.get(userId); if (!session) { throw new Error('Keine aktive Session'); } // In DB speichern try { const db = getDb(); db.prepare(` INSERT INTO assistant_messages (session_id, role, content) VALUES (?, 'user', ?) `).run(session.sessionId, message); } catch (err) { logger.error(`[Assistant] Fehler beim Speichern der Nachricht: ${err.message}`); } // An Prozess senden session.process.stdin.write(message + '\n'); resetTimeout(userId); } /** * Session beenden */ function stopSession(userId) { const session = activeSessions.get(userId); if (!session) return; logger.info(`[Assistant] Session ${session.sessionId} wird beendet fuer User ${userId}`); // Restlichen Buffer speichern flushOutputBuffer(userId); // Timer aufraemen if (session.timeout) clearTimeout(session.timeout); if (session.saveTimer) clearTimeout(session.saveTimer); // Prozess beenden try { session.process.kill('SIGTERM'); } catch (err) { logger.error(`[Assistant] Fehler beim Killen des Prozesses: ${err.message}`); } activeSessions.delete(userId); // DB aktualisieren try { const db = getDb(); db.prepare(` UPDATE assistant_sessions SET status = 'ended', ended_at = CURRENT_TIMESTAMP WHERE id = ? `).run(session.sessionId); } catch (err) { logger.error(`[Assistant] Fehler beim DB-Update: ${err.message}`); } } // ============================================================================ // REST ENDPOINTS // ============================================================================ // Alle Routen brauchen Assistant-Zugriff router.use(requireAssistantAccess); /** * GET /sessions - Alle Sessions des Users */ router.get('/sessions', (req, res) => { try { const db = getDb(); const sessions = db.prepare(` SELECT id, user_id, title, status, task_context, created_at, ended_at FROM assistant_sessions WHERE user_id = ? ORDER BY created_at DESC `).all(req.user.id); res.json(sessions); } catch (err) { logger.error(`[Assistant] Fehler beim Laden der Sessions: ${err.message}`); res.status(500).json({ error: 'Fehler beim Laden der Sessions' }); } }); /** * GET /sessions/:id/messages - Alle Nachrichten einer Session */ router.get('/sessions/:id/messages', (req, res) => { try { const db = getDb(); const sessionId = parseInt(req.params.id, 10); // Pruefen ob Session dem User gehoert const session = db.prepare(` SELECT id FROM assistant_sessions WHERE id = ? AND user_id = ? `).get(sessionId, req.user.id); if (!session) { return res.status(404).json({ error: 'Session nicht gefunden' }); } const messages = db.prepare(` SELECT id, session_id, role, content, created_at FROM assistant_messages WHERE session_id = ? ORDER BY created_at ASC `).all(sessionId); res.json(messages); } catch (err) { logger.error(`[Assistant] Fehler beim Laden der Nachrichten: ${err.message}`); res.status(500).json({ error: 'Fehler beim Laden der Nachrichten' }); } }); /** * POST /sessions - Neue Session erstellen */ router.post('/sessions', (req, res) => { try { const db = getDb(); const { title, taskContext } = req.body; const result = db.prepare(` INSERT INTO assistant_sessions (user_id, title, task_context) VALUES (?, ?, ?) `).run(req.user.id, title || 'Neue Session', taskContext || null); const session = db.prepare(` SELECT id, user_id, title, status, task_context, created_at, ended_at FROM assistant_sessions WHERE id = ? `).get(result.lastInsertRowid); logger.info(`[Assistant] Neue Session ${session.id} erstellt von ${req.user.username}`); res.status(201).json(session); } catch (err) { logger.error(`[Assistant] Fehler beim Erstellen der Session: ${err.message}`); res.status(500).json({ error: 'Fehler beim Erstellen der Session' }); } }); /** * DELETE /sessions/:id - Session loeschen */ router.delete('/sessions/:id', (req, res) => { try { const db = getDb(); const sessionId = parseInt(req.params.id, 10); // Pruefen ob Session dem User gehoert const session = db.prepare(` SELECT id, user_id FROM assistant_sessions WHERE id = ? AND user_id = ? `).get(sessionId, req.user.id); if (!session) { return res.status(404).json({ error: 'Session nicht gefunden' }); } // Wenn aktiver Prozess laeuft, zuerst beenden const activeSession = activeSessions.get(req.user.id); if (activeSession && activeSession.sessionId === sessionId) { stopSession(req.user.id); } // Session und zugehoerige Nachrichten loeschen (CASCADE) db.prepare('DELETE FROM assistant_sessions WHERE id = ?').run(sessionId); logger.info(`[Assistant] Session ${sessionId} geloescht von ${req.user.username}`); res.json({ success: true }); } catch (err) { logger.error(`[Assistant] Fehler beim Loeschen der Session: ${err.message}`); res.status(500).json({ error: 'Fehler beim Loeschen der Session' }); } }); // ============================================================================ // SOCKET EVENT HANDLER (exportiert fuer server.js) // ============================================================================ /** * Socket-Events registrieren */ function registerSocketEvents(socket) { const userId = socket.user.id; // assistant:start - Session starten socket.on('assistant:start', (data) => { try { if (!checkSocketAccess(socket)) { socket.emit('assistant:status', { status: 'error', error: 'Kein Zugriff' }); return; } const sessionId = data && data.sessionId; if (!sessionId) { socket.emit('assistant:status', { status: 'error', error: 'Keine Session-ID angegeben' }); return; } // Pruefen ob Session existiert und dem User gehoert const db = getDb(); const session = db.prepare(` SELECT id, user_id, status FROM assistant_sessions WHERE id = ? AND user_id = ? `).get(sessionId, userId); if (!session) { socket.emit('assistant:status', { status: 'error', error: 'Session nicht gefunden' }); return; } startSession(userId, sessionId, socket); socket.emit('assistant:status', { sessionId, status: 'running' }); } catch (err) { logger.error(`[Assistant] Fehler beim Starten: ${err.message}`); socket.emit('assistant:status', { status: 'error', error: err.message }); } }); // assistant:message - Nachricht senden socket.on('assistant:message', (data) => { try { if (!checkSocketAccess(socket)) { socket.emit('assistant:status', { status: 'error', error: 'Kein Zugriff' }); return; } const message = data && data.message; if (!message || typeof message !== 'string') { socket.emit('assistant:status', { status: 'error', error: 'Keine Nachricht angegeben' }); return; } sendMessage(userId, message); } catch (err) { logger.error(`[Assistant] Fehler beim Senden: ${err.message}`); socket.emit('assistant:status', { status: 'error', error: err.message }); } }); // assistant:stop - Session beenden socket.on('assistant:stop', () => { try { if (!checkSocketAccess(socket)) { socket.emit('assistant:status', { status: 'error', error: 'Kein Zugriff' }); return; } stopSession(userId); socket.emit('assistant:status', { status: 'stopped' }); } catch (err) { logger.error(`[Assistant] Fehler beim Stoppen: ${err.message}`); socket.emit('assistant:status', { status: 'error', error: err.message }); } }); // Bei Disconnect: aktive Session beenden socket.on('disconnect', () => { if (activeSessions.has(userId)) { logger.info(`[Assistant] Socket disconnect - Session wird beendet fuer User ${userId}`); stopSession(userId); } }); } module.exports = router; module.exports.registerSocketEvents = registerSocketEvents; module.exports.stopSession = stopSession;