claude-desktop/pwa/server/index.js
Eddy 4e36b04cc9
All checks were successful
Build AppImage / build (push) Has been skipped
PWA Mobile-App: API-Server + SvelteKit-Frontend (Phase 1+2)
Backend (pwa/server/):
- Express + WebSocket API-Server auf Port 3100
- Claude Agent SDK Bridge mit Streaming
- Bearer-Token Authentifizierung
- REST: /api/status, /api/models, /api/sessions, /api/stop
- WebSocket: /ws mit Live-Text-Streaming
- Dockerfile für Container-Deployment

Frontend (pwa/client/):
- SvelteKit 5 PWA mit Dark Theme
- Mobil-optimierter Chat (WhatsApp/Telegram-Feeling)
- Message-Bubbles mit Markdown + Live-Streaming
- Session-Drawer (Swipe von links)
- Settings-Modal (Server/Token/Modell)
- Service Worker für Auto-Updates
- PWA-Manifest für "Add to Homescreen"
- Safe-Area-Insets für Notch-Handys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 06:38:12 +02:00

260 lines
6.4 KiB
JavaScript

// Claude Chat API — Express + WebSocket Server
//
// Exponiert die Claude Bridge als HTTP-REST-API und WebSocket fuer Streaming.
// Port: 3100 (ENV: PORT)
import express from 'express';
import cors from 'cors';
import { createServer } from 'node:http';
import { WebSocketServer } from 'ws';
import { authMiddleware, verifyWsToken } from './auth.js';
import {
sendMessage,
stopAll,
getStatus,
listModels,
setModel,
setMode,
createSession,
listSessions,
getSession,
} from './bridge.js';
const PORT = parseInt(process.env.PORT || '3100', 10);
const app = express();
const server = createServer(app);
// ============ Middleware ============
app.use(cors());
app.use(express.json());
// ============ REST Endpoints ============
// Status — oeffentlich fuer Health-Checks (optional: mit Auth schuetzen)
app.get('/api/status', authMiddleware, (req, res) => {
const status = getStatus();
res.json(status);
});
// Modelle auflisten
app.get('/api/models', authMiddleware, (req, res) => {
res.json(listModels());
});
// Modell setzen
app.post('/api/model', authMiddleware, (req, res) => {
const { model } = req.body;
if (!model) {
return res.status(400).json({ error: 'Feld "model" fehlt' });
}
try {
const result = setModel(model);
res.json(result);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// Modus setzen
app.post('/api/mode', authMiddleware, (req, res) => {
const { mode } = req.body;
if (!mode) {
return res.status(400).json({ error: 'Feld "mode" fehlt' });
}
try {
const result = setMode(mode);
res.json(result);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// Sessions auflisten
app.get('/api/sessions', authMiddleware, (req, res) => {
res.json(listSessions());
});
// Neue Session erstellen
app.post('/api/sessions', authMiddleware, (req, res) => {
const { title } = req.body || {};
const session = createSession(title);
res.status(201).json(session);
});
// Einzelne Session abrufen
app.get('/api/sessions/:id', authMiddleware, (req, res) => {
const session = getSession(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session nicht gefunden' });
}
res.json(session);
});
// Stopp-Endpoint (auch ueber REST erreichbar)
app.post('/api/stop', authMiddleware, (req, res) => {
const stopped = stopAll();
res.json({ stopped });
});
// ============ WebSocket ============
const wss = new WebSocketServer({ noServer: true });
// Upgrade-Handler: Token aus Query-Parameter pruefen
server.on('upgrade', (request, socket, head) => {
// Nur /ws akzeptieren
const url = new URL(request.url, `http://${request.headers.host}`);
if (url.pathname !== '/ws') {
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
socket.destroy();
return;
}
// Token pruefen
if (!verifyWsToken(request.url)) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
});
wss.on('connection', (ws) => {
console.log('[ws] Client verbunden');
ws.on('message', (data) => {
let msg;
try {
msg = JSON.parse(data.toString());
} catch {
ws.send(JSON.stringify({ type: 'error', message: 'Ungueltiges JSON' }));
return;
}
switch (msg.type) {
case 'message': {
if (!msg.content) {
ws.send(JSON.stringify({ type: 'error', message: 'Feld "content" fehlt' }));
return;
}
const emitter = sendMessage(msg.content, msg.model, msg.mode, msg.sessionId);
// Alle Events an den WebSocket-Client weiterleiten
emitter.on('text', (payload) => {
safeSend(ws, { type: 'text', content: payload.content });
});
emitter.on('tool_call', (payload) => {
safeSend(ws, {
type: 'tool_call',
name: payload.name,
args: payload.args,
id: payload.id,
summary: payload.summary,
});
});
emitter.on('tool_result', (payload) => {
safeSend(ws, {
type: 'tool_result',
id: payload.id,
success: payload.success,
});
});
emitter.on('agent_started', (payload) => {
safeSend(ws, {
type: 'agent_started',
id: payload.id,
name: payload.name || payload.type,
});
});
emitter.on('agent_stopped', (payload) => {
safeSend(ws, {
type: 'agent_stopped',
id: payload.id,
success: payload.success,
});
});
emitter.on('result', (payload) => {
safeSend(ws, {
type: 'result',
content: payload.content,
cost: payload.cost,
tokens: payload.tokens,
session_id: payload.session_id,
duration_ms: payload.duration_ms,
model: payload.model,
});
});
emitter.on('error', (payload) => {
safeSend(ws, { type: 'error', message: payload.message });
});
break;
}
case 'stop': {
const stopped = stopAll();
safeSend(ws, { type: 'stopped', success: stopped });
break;
}
default:
safeSend(ws, { type: 'error', message: `Unbekannter Nachrichtentyp: ${msg.type}` });
}
});
ws.on('close', () => {
console.log('[ws] Client getrennt');
});
ws.on('error', (err) => {
console.error('[ws] Fehler:', err.message);
});
// Willkommens-Nachricht
safeSend(ws, {
type: 'connected',
status: getStatus(),
models: listModels(),
});
});
/** Sicher an WebSocket senden (nur wenn offen) */
function safeSend(ws, data) {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify(data));
}
}
// ============ Server starten ============
server.listen(PORT, () => {
console.log(`[claude-chat-api] Server laeuft auf Port ${PORT}`);
console.log(` REST: http://localhost:${PORT}/api/status`);
console.log(` WebSocket: ws://localhost:${PORT}/ws?token=<TOKEN>`);
});
// Graceful Shutdown
process.on('SIGTERM', () => {
console.log('[server] SIGTERM empfangen, fahre herunter...');
stopAll();
wss.close();
server.close(() => process.exit(0));
});
process.on('SIGINT', () => {
console.log('[server] SIGINT empfangen, fahre herunter...');
stopAll();
wss.close();
server.close(() => process.exit(0));
});