getHeaderLine('Authorization'); if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) { $response = new \Slim\Psr7\Response(); $response->getBody()->write(json_encode(['error' => 'Unauthorized'])); return $response->withStatus(401)->withHeader('Content-Type', 'application/json'); } $token = substr($authHeader, 7); $decoded = jwtVerify($token); if (!$decoded) { $response = new \Slim\Psr7\Response(); $response->getBody()->write(json_encode(['error' => 'Invalid token'])); return $response->withStatus(401)->withHeader('Content-Type', 'application/json'); } $request = $request->withAttribute('userId', $decoded['userId']); return $handler->handle($request); }; // Health check $app->get('/api/health', function (Request $request, Response $response) { $response->getBody()->write(json_encode(['status' => 'ok'])); return $response->withHeader('Content-Type', 'application/json'); }); // Auth routes $app->post('/api/auth/login', function (Request $request, Response $response) { $body = json_decode((string)$request->getBody(), true); $password = $body['password'] ?? ''; $config = require __DIR__ . '/config.php'; if (!$password) { $response->getBody()->write(json_encode(['error' => 'Password required'])); return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); } if ($password !== $config['password']) { $response->getBody()->write(json_encode(['error' => 'Invalid password'])); return $response->withStatus(401)->withHeader('Content-Type', 'application/json'); } $userId = 'admin'; $token = jwtGenerate($userId); $response->getBody()->write(json_encode([ 'token' => $token, 'user' => ['id' => $userId, 'username' => 'admin'] ])); return $response->withHeader('Content-Type', 'application/json'); }); $app->get('/api/auth/me', function (Request $request, Response $response) { $userId = $request->getAttribute('userId'); $response->getBody()->write(json_encode([ 'id' => $userId, 'username' => 'admin' ])); return $response->withHeader('Content-Type', 'application/json'); })->add($authMiddleware); // Conversation routes $app->get('/api/conversations', function (Request $request, Response $response) { $userId = $request->getAttribute('userId'); $db = getDb(); $stmt = $db->prepare("SELECT id, title, model_name, model_base_url, model_id, temperature, max_tokens, created_at, updated_at FROM conversations WHERE user_id = ? ORDER BY updated_at DESC"); $stmt->execute([$userId]); $rows = $stmt->fetchAll(); $response->getBody()->write(json_encode($rows)); return $response->withHeader('Content-Type', 'application/json'); })->add($authMiddleware); $app->get('/api/conversations/{id}', function (Request $request, Response $response, array $args) { $userId = $request->getAttribute('userId'); $id = $args['id']; $db = getDb(); $stmt = $db->prepare('SELECT * FROM conversations WHERE id = ? AND user_id = ?'); $stmt->execute([$id, $userId]); $conv = $stmt->fetch(); if (!$conv) { $response->getBody()->write(json_encode(['error' => 'Conversation not found'])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } $stmt = $db->prepare('SELECT id, role, content, timestamp FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC'); $stmt->execute([$id]); $messages = $stmt->fetchAll(); $response->getBody()->write(json_encode(array_merge($conv, ['messages' => $messages]))); return $response->withHeader('Content-Type', 'application/json'); })->add($authMiddleware); $app->post('/api/conversations', function (Request $request, Response $response) { $userId = $request->getAttribute('userId'); $body = json_decode((string)$request->getBody(), true); $title = $body['title'] ?? '新对话'; $modelConfig = $body['modelConfig'] ?? null; $db = getDb(); $id = bin2hex(random_bytes(16)); $now = time() * 1000; $stmt = $db->prepare('INSERT INTO conversations (id, user_id, title, model_name, model_base_url, model_id, api_key, temperature, max_tokens, system_prompt, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'); $stmt->execute([ $id, $userId, $title, $modelConfig['name'] ?? null, $modelConfig['baseUrl'] ?? null, $modelConfig['model'] ?? null, $modelConfig['apiKey'] ?? null, $modelConfig['temperature'] ?? 0.7, $modelConfig['maxTokens'] ?? 4096, $modelConfig['systemPrompt'] ?? null, $now, $now ]); $stmt = $db->prepare('SELECT * FROM conversations WHERE id = ?'); $stmt->execute([$id]); $conv = $stmt->fetch(); $response->getBody()->write(json_encode(array_merge($conv, ['messages' => []]))); return $response->withStatus(201)->withHeader('Content-Type', 'application/json'); })->add($authMiddleware); $app->put('/api/conversations/{id}', function (Request $request, Response $response, array $args) { $userId = $request->getAttribute('userId'); $id = $args['id']; $body = json_decode((string)$request->getBody(), true); $title = $body['title'] ?? null; $modelConfig = $body['modelConfig'] ?? null; $db = getDb(); $stmt = $db->prepare('SELECT id FROM conversations WHERE id = ? AND user_id = ?'); $stmt->execute([$id, $userId]); if (!$stmt->fetch()) { $response->getBody()->write(json_encode(['error' => 'Conversation not found'])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } $now = time() * 1000; $stmt = $db->prepare('UPDATE conversations SET title = COALESCE(?, title), model_name = COALESCE(?, model_name), model_base_url = COALESCE(?, model_base_url), model_id = COALESCE(?, model_id), api_key = COALESCE(?, api_key), temperature = COALESCE(?, temperature), max_tokens = COALESCE(?, max_tokens), system_prompt = COALESCE(?, system_prompt), updated_at = ? WHERE id = ?'); $stmt->execute([ $title, $modelConfig['name'] ?? null, $modelConfig['baseUrl'] ?? null, $modelConfig['model'] ?? null, $modelConfig['apiKey'] ?? null, $modelConfig['temperature'] ?? null, $modelConfig['maxTokens'] ?? null, $modelConfig['systemPrompt'] ?? null, $now, $id ]); $stmt = $db->prepare('SELECT * FROM conversations WHERE id = ?'); $stmt->execute([$id]); $conv = $stmt->fetch(); $stmt = $db->prepare('SELECT id, role, content, timestamp FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC'); $stmt->execute([$id]); $messages = $stmt->fetchAll(); $response->getBody()->write(json_encode(array_merge($conv, ['messages' => $messages]))); return $response->withHeader('Content-Type', 'application/json'); })->add($authMiddleware); $app->delete('/api/conversations/{id}', function (Request $request, Response $response, array $args) { $userId = $request->getAttribute('userId'); $id = $args['id']; $db = getDb(); $stmt = $db->prepare('DELETE FROM conversations WHERE id = ? AND user_id = ?'); $stmt->execute([$id, $userId]); if ($stmt->rowCount() === 0) { $response->getBody()->write(json_encode(['error' => 'Conversation not found'])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } $response->getBody()->write(json_encode(['success' => true])); return $response->withHeader('Content-Type', 'application/json'); })->add($authMiddleware); $app->delete('/api/conversations/{id}/messages', function (Request $request, Response $response, array $args) { $userId = $request->getAttribute('userId'); $id = $args['id']; $db = getDb(); $stmt = $db->prepare('SELECT id FROM conversations WHERE id = ? AND user_id = ?'); $stmt->execute([$id, $userId]); if (!$stmt->fetch()) { $response->getBody()->write(json_encode(['error' => 'Conversation not found'])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } $stmt = $db->prepare('DELETE FROM messages WHERE conversation_id = ?'); $stmt->execute([$id]); $now = time() * 1000; $stmt = $db->prepare('UPDATE conversations SET updated_at = ? WHERE id = ?'); $stmt->execute([$now, $id]); $response->getBody()->write(json_encode(['success' => true])); return $response->withHeader('Content-Type', 'application/json'); })->add($authMiddleware); // Chat route (SSE) $app->post('/api/chat', function (Request $request, Response $response) { $userId = $request->getAttribute('userId'); $body = json_decode((string)$request->getBody(), true); $conversationId = $body['conversationId'] ?? ''; $content = trim($body['content'] ?? ''); if (!$conversationId || !$content) { $response->getBody()->write(json_encode(['error' => 'conversationId and content required'])); return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); } $db = getDb(); $stmt = $db->prepare('SELECT * FROM conversations WHERE id = ? AND user_id = ?'); $stmt->execute([$conversationId, $userId]); $conv = $stmt->fetch(); if (!$conv) { $response->getBody()->write(json_encode(['error' => 'Conversation not found'])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } // Load history $stmt = $db->prepare("SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC"); $stmt->execute([$conversationId]); $history = $stmt->fetchAll(); // Build messages $messages = []; if ($conv['system_prompt']) { $messages[] = ['role' => 'system', 'content' => $conv['system_prompt']]; } foreach ($history as $msg) { if ($msg['role'] !== 'system') { $messages[] = ['role' => $msg['role'], 'content' => $msg['content']]; } } $messages[] = ['role' => 'user', 'content' => $content]; // Save user message $userMsgId = bin2hex(random_bytes(16)); $now = time() * 1000; $stmt = $db->prepare('INSERT INTO messages (id, conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?, ?)'); $stmt->execute([$userMsgId, $conversationId, 'user', $content, $now]); // Update title if first message $stmt = $db->prepare('SELECT COUNT(*) as cnt FROM messages WHERE conversation_id = ?'); $stmt->execute([$conversationId]); $msgCount = $stmt->fetch()['cnt'] ?? 0; if ($msgCount == 1) { $newTitle = mb_substr($content, 0, 20) . (mb_strlen($content) > 20 ? '...' : ''); $stmt = $db->prepare('UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?'); $stmt->execute([$newTitle, $now, $conversationId]); } // Check model config if (!$conv['model_base_url'] || !$conv['api_key'] || !$conv['model_id']) { $errMsg = 'Error: 模型配置不完整,请先在「配置」中设置 API 地址、API Key 和模型 ID'; $errMsgId = bin2hex(random_bytes(16)); $stmt = $db->prepare('INSERT INTO messages (id, conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?, ?)'); $stmt->execute([$errMsgId, $conversationId, 'assistant', $errMsg, $now]); $response->getBody()->write(json_encode(['error' => 'Model config incomplete'])); return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); } // SSE output header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); while (ob_get_level() > 0) ob_end_flush(); ob_implicit_flush(true); // Send user message confirmation echo "data: " . json_encode(['type' => 'user', 'id' => $userMsgId, 'content' => $content]) . "\n\n"; flush(); // Call LLM API via curl $assistantMsgId = bin2hex(random_bytes(16)); $fullContent = ''; $payload = json_encode([ 'model' => $conv['model_id'], 'messages' => $messages, 'temperature' => (float)$conv['temperature'], 'max_tokens' => (int)$conv['max_tokens'], 'stream' => true, ]); $ch = curl_init($conv['model_base_url'] . '/chat/completions'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $conv['api_key'], ], CURLOPT_RETURNTRANSFER => false, CURLOPT_WRITEFUNCTION => function ($ch, $data) use (&$fullContent) { $lines = explode("\n", $data); foreach ($lines as $line) { $line = trim($line); if (!$line || !str_starts_with($line, 'data: ')) continue; $jsonStr = substr($line, 6); if ($jsonStr === '[DONE]') continue; $parsed = json_decode($jsonStr, true); if (!$parsed) continue; $delta = $parsed['choices'][0]['delta']['content'] ?? ''; if ($delta) { $fullContent .= $delta; echo "data: " . json_encode(['type' => 'delta', 'content' => $delta]) . "\n\n"; flush(); } } return strlen($data); }, CURLOPT_TIMEOUT => 120, CURLOPT_CONNECTTIMEOUT => 30, ]); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_exec($ch); $error = curl_error($ch); curl_close($ch); if ($error) { $errMsg = 'Error: ' . $error; echo "data: " . json_encode(['type' => 'error', 'content' => $errMsg]) . "\n\n"; } // Save assistant message $stmt = $db->prepare('INSERT INTO messages (id, conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?, ?)'); $stmt->execute([$assistantMsgId, $conversationId, 'assistant', $fullContent, time() * 1000]); $stmt = $db->prepare('UPDATE conversations SET updated_at = ? WHERE id = ?'); $stmt->execute([time() * 1000, $conversationId]); echo "data: " . json_encode(['type' => 'done', 'id' => $assistantMsgId, 'content' => $fullContent]) . "\n\n"; echo "data: [DONE]\n\n"; flush(); exit(0); })->add($authMiddleware);