chore: remove deprecated files and configurations
- Deleted .gitignore, biome.json, next.config.ts, package.json, pnpm-lock.yaml, pnpm-workspace.yaml, postcss.config.mjs, README.md, and various API, component, and utility files to clean up the codebase. - Removed unused database files and migration scripts to streamline the project structure. - Eliminated obsolete hooks and services to enhance maintainability and reduce clutter.
This commit is contained in:
1 parent
a8bb7cc22d
commit
ad9d5d32d2
119 files changed
-17633
No files matched your search
@@ -1,77 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDatabase } from "@/database/client";
|
||||
import { MessageRepository } from "@/database/repositories/message";
|
||||
import { ConversationRepository } from "@/database/repositories/conversation";
|
||||
import { addMessageEmbedding } from "@/lib/vector-search";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const url = new URL(request.url);
|
||||
const limitParam = url.searchParams.get("limit");
|
||||
const beforeParam = url.searchParams.get("before");
|
||||
|
||||
const limit = Math.min(Math.max(Number(limitParam) || 0, 1), 200) || 50;
|
||||
const beforeTs = beforeParam ? Number(beforeParam) : undefined;
|
||||
|
||||
const db = getDatabase();
|
||||
const messageRepo = new MessageRepository(db);
|
||||
|
||||
const messages = messageRepo.findByConversationPaged(id, limit, beforeTs);
|
||||
const hasMore = messages.length === limit;
|
||||
const cursor = messages.length > 0 ? messages[0].timestamp : null;
|
||||
|
||||
return NextResponse.json({ messages, hasMore, cursor });
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar mensagens:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro interno do servidor" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { role, content } = await request.json();
|
||||
|
||||
if (!role || !content) {
|
||||
return NextResponse.json(
|
||||
{ error: "Role e content são obrigatórios" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
const messageRepo = new MessageRepository(db);
|
||||
const conversationRepo = new ConversationRepository(db);
|
||||
|
||||
// Criar mensagem
|
||||
const messageId = messageRepo.create(id, role, content);
|
||||
|
||||
// Atualizar timestamp da conversa
|
||||
conversationRepo.updateTimestamp(id);
|
||||
|
||||
// Gerar embedding em background (apenas para mensagens do usuário e assistente)
|
||||
if (role === "user" || role === "assistant") {
|
||||
addMessageEmbedding(messageId, content).catch((error) => {
|
||||
console.error("Erro ao gerar embedding:", error);
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ id: messageId });
|
||||
} catch (error) {
|
||||
console.error("Erro ao criar mensagem:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro interno do servidor" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDatabase } from "@/database/client";
|
||||
import { ConversationRepository } from "@/database/repositories/conversation";
|
||||
import { MessageRepository } from "@/database/repositories/message";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const db = getDatabase();
|
||||
const conversationRepo = new ConversationRepository(db);
|
||||
const messageRepo = new MessageRepository(db);
|
||||
|
||||
const conversation = conversationRepo.findById(id);
|
||||
if (!conversation) {
|
||||
return NextResponse.json(
|
||||
{ error: "Conversa não encontrada" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const messages = messageRepo.findByConversation(id);
|
||||
|
||||
return NextResponse.json({
|
||||
conversation,
|
||||
messages,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar conversa:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro interno do servidor" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const db = getDatabase();
|
||||
const conversationRepo = new ConversationRepository(db);
|
||||
|
||||
conversationRepo.delete(id);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Erro ao deletar conversa:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro interno do servidor" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDatabase } from "@/database/client";
|
||||
import { ConversationRepository } from "@/database/repositories/conversation";
|
||||
import { MessageRepository } from "@/database/repositories/message";
|
||||
import { generateChatTitle } from "@/lib/embeddings";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const conversationRepo = new ConversationRepository(db);
|
||||
|
||||
const conversations = conversationRepo.findAll();
|
||||
|
||||
return NextResponse.json(conversations);
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar conversas:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro interno do servidor" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { model, firstMessage } = await request.json();
|
||||
|
||||
if (!model) {
|
||||
return NextResponse.json(
|
||||
{ error: "Modelo é obrigatório" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
const conversationRepo = new ConversationRepository(db);
|
||||
|
||||
// Gerar título se primeira mensagem fornecida
|
||||
let title = "Nova conversa";
|
||||
if (firstMessage) {
|
||||
title = await generateChatTitle(firstMessage);
|
||||
}
|
||||
|
||||
const conversationId = conversationRepo.create(model, title);
|
||||
|
||||
return NextResponse.json({ id: conversationId, title });
|
||||
} catch (error) {
|
||||
console.error("Erro ao criar conversa:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro interno do servidor" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* API endpoint para listar MCPs disponíveis para uso no chat
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPExecutor } from "@/lib/services/mcp-executor";
|
||||
import type { MCPTool } from "@/lib/types/mcp-chat";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Buscar todos os MCPs instalados com status "ready"
|
||||
const allMcps = MCPRepository.getAllInstalled();
|
||||
|
||||
const availableMcps = allMcps
|
||||
.filter((mcp: any) => !mcp.status || mcp.status === "ready")
|
||||
.map((mcp: any) => {
|
||||
const tools = MCPExecutor.getAvailableTools(mcp.id);
|
||||
|
||||
// Converter tools para formato OpenAI function calling
|
||||
const toolsFormatted = tools.map((tool: MCPTool) => ({
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: `${mcp.id}__${tool.name}`, // Prefixar com mcpId para evitar conflitos
|
||||
description: tool.description,
|
||||
parameters: tool.inputSchema,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
mcpId: mcp.id,
|
||||
name: mcp.name,
|
||||
owner: mcp.owner,
|
||||
repo: mcp.repo,
|
||||
toolCount: tools.length,
|
||||
tools: toolsFormatted,
|
||||
rawTools: tools, // Tools originais para referência
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: availableMcps,
|
||||
count: availableMcps.length,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error listing available MCPs:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to list available MCPs",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/**
|
||||
* API endpoint para executar um tool de MCP
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { MCPExecutor } from "@/lib/services/mcp-executor";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { mcpId, toolName, parameters } = body;
|
||||
|
||||
// Validar parâmetros
|
||||
if (!mcpId || !toolName) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Missing mcpId or toolName",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`📞 API call to execute tool: ${mcpId}.${toolName}`);
|
||||
|
||||
// Obter tools disponíveis
|
||||
const availableTools = MCPExecutor.getAvailableTools(mcpId);
|
||||
const tool = availableTools.find((t) => t.name === toolName);
|
||||
|
||||
if (!tool) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Tool ${toolName} not found in MCP ${mcpId}`,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validar parâmetros contra schema
|
||||
const validation = MCPExecutor.validateParameters(parameters || {}, tool);
|
||||
if (!validation.valid) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Invalid parameters: ${validation.error}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Executar tool
|
||||
const result = await MCPExecutor.executeMCPTool(
|
||||
mcpId,
|
||||
toolName,
|
||||
parameters || {}
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: result.error,
|
||||
executionTime: result.executionTime,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcpId,
|
||||
toolName,
|
||||
result: result.result,
|
||||
executionTime: result.executionTime,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error executing MCP tool:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to execute MCP tool",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPInstallerService } from "@/lib/services/mcp-installer";
|
||||
import { MCPValidatorService } from "@/lib/services/mcp-validator";
|
||||
import {
|
||||
MCPInstallationStatus,
|
||||
type InstallationConfig,
|
||||
type InstallationProgress,
|
||||
type PackageRegistry,
|
||||
} from "@/lib/types/mcp-installer";
|
||||
|
||||
/**
|
||||
* POST /api/mcp/install-manual
|
||||
* Instala um MCP a partir de uma configuração manual (formato Claude Desktop)
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { config, mcpId, enableLogs } = body;
|
||||
|
||||
// Validar estrutura básica
|
||||
if (!config || typeof config !== "object") {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Invalid configuration format" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Extrair primeiro servidor da configuração (se for formato mcpServers)
|
||||
let serverConfig: any;
|
||||
let serverId: string;
|
||||
|
||||
if (config.mcpServers) {
|
||||
// Formato: { "mcpServers": { "server-name": { ... } } }
|
||||
const serverNames = Object.keys(config.mcpServers);
|
||||
if (serverNames.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "No MCP server found in configuration" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
serverId = mcpId || serverNames[0];
|
||||
serverConfig = config.mcpServers[serverNames[0]];
|
||||
} else {
|
||||
// Formato direto: { "command": "...", "args": [...] }
|
||||
serverId = mcpId || "manual-mcp";
|
||||
serverConfig = config;
|
||||
}
|
||||
|
||||
// Validar campos obrigatórios
|
||||
if (!serverConfig.command) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Missing required field: command" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🚀 Starting manual installation for: ${serverId}`);
|
||||
console.log(
|
||||
`Command: ${serverConfig.command} ${(serverConfig.args || []).join(" ")}`
|
||||
);
|
||||
|
||||
// Detectar tipo de pacote baseado no comando
|
||||
const { packageRegistry, packageName, repositoryUrl } =
|
||||
detectPackageInfo(serverConfig);
|
||||
|
||||
console.log(`Detected package type: ${packageRegistry} (${packageName})`);
|
||||
|
||||
// Verificar se já está instalado
|
||||
if (MCPRepository.isInstalled(serverId)) {
|
||||
const existing = MCPRepository.getInstalledMCP(serverId);
|
||||
|
||||
if (existing.status === "ready") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `MCP ${serverId} is already installed`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Re-installing MCP ${serverId} (current status: ${existing.status})`
|
||||
);
|
||||
}
|
||||
|
||||
// Preparar configuração de instalação
|
||||
const installConfig: InstallationConfig = {
|
||||
mcpId: serverId,
|
||||
packageName,
|
||||
packageRegistry,
|
||||
repositoryUrl,
|
||||
config: serverConfig,
|
||||
enableLogs: enableLogs === true,
|
||||
};
|
||||
|
||||
// Salvar no banco como PENDING
|
||||
if (!MCPRepository.isInstalled(serverId)) {
|
||||
MCPRepository.saveInstallation(serverId, serverConfig, []);
|
||||
}
|
||||
MCPRepository.updateInstallationStatus(
|
||||
serverId,
|
||||
"pending",
|
||||
"Starting manual installation..."
|
||||
);
|
||||
|
||||
// Iniciar instalação em background
|
||||
installInBackground(installConfig);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Manual installation started",
|
||||
mcpId: serverId,
|
||||
status: "pending",
|
||||
detectedType: packageRegistry,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error starting manual installation:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to start manual installation",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta informações do pacote baseado no comando
|
||||
*/
|
||||
function detectPackageInfo(serverConfig: any): {
|
||||
packageRegistry: PackageRegistry;
|
||||
packageName: string;
|
||||
repositoryUrl?: string;
|
||||
} {
|
||||
const command = serverConfig.command.toLowerCase();
|
||||
const args = serverConfig.args || [];
|
||||
const firstArg = args[0] || "";
|
||||
|
||||
// NPM/NPX
|
||||
if (command === "npx" || command === "npm") {
|
||||
// npx @mcpcentral/mcp-time
|
||||
// npx -y @modelcontextprotocol/server-filesystem
|
||||
// npx -y github:user/repo
|
||||
const packageArg = args.find(
|
||||
(arg: string) =>
|
||||
arg.startsWith("@") || arg.startsWith("github:") || !arg.startsWith("-")
|
||||
);
|
||||
let packageName = packageArg || firstArg || "unknown-npm-package";
|
||||
packageName = packageName.replace(/^-y\s*/, "").trim();
|
||||
|
||||
// Converter github:user/repo para git+https://github.com/user/repo.git
|
||||
// Isso evita problemas com SSH e funciona melhor com npm install
|
||||
if (packageName.startsWith("github:")) {
|
||||
const repoPath = packageName.replace("github:", "");
|
||||
packageName = `git+https://github.com/${repoPath}.git`;
|
||||
console.log(`Converted github: format to: ${packageName}`);
|
||||
}
|
||||
|
||||
return {
|
||||
packageRegistry: "npm",
|
||||
packageName,
|
||||
};
|
||||
}
|
||||
|
||||
// Node
|
||||
if (command === "node" || command.endsWith("node")) {
|
||||
// node dist/index.js -> assume que é local ou já instalado
|
||||
return {
|
||||
packageRegistry: "npm",
|
||||
packageName: "local-node-package",
|
||||
};
|
||||
}
|
||||
|
||||
// Python
|
||||
if (
|
||||
command === "python" ||
|
||||
command === "python3" ||
|
||||
command.includes("python")
|
||||
) {
|
||||
// python -m mcp_server_git
|
||||
// python /path/to/server.py
|
||||
const moduleIndex = args.indexOf("-m");
|
||||
if (moduleIndex !== -1 && args[moduleIndex + 1]) {
|
||||
return {
|
||||
packageRegistry: "pypi",
|
||||
packageName: args[moduleIndex + 1],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
packageRegistry: "pypi",
|
||||
packageName: "local-python-package",
|
||||
};
|
||||
}
|
||||
|
||||
// UV (Python package manager)
|
||||
if (command === "uv" || command === "uvx") {
|
||||
// uvx mcp-server-git
|
||||
const packageName = args[0] || "unknown-uv-package";
|
||||
return {
|
||||
packageRegistry: "pypi",
|
||||
packageName,
|
||||
};
|
||||
}
|
||||
|
||||
// Cargo/Rust
|
||||
if (command === "cargo") {
|
||||
// cargo run --bin mcp-server
|
||||
const binIndex = args.indexOf("--bin");
|
||||
if (binIndex !== -1 && args[binIndex + 1]) {
|
||||
return {
|
||||
packageRegistry: "cargo",
|
||||
packageName: args[binIndex + 1],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
packageRegistry: "cargo",
|
||||
packageName: "local-rust-package",
|
||||
};
|
||||
}
|
||||
|
||||
// Docker
|
||||
if (command === "docker") {
|
||||
return {
|
||||
packageRegistry: "other",
|
||||
packageName: "docker-container",
|
||||
};
|
||||
}
|
||||
|
||||
// Caminho absoluto ou relativo (executável local)
|
||||
if (command.startsWith("/") || command.startsWith("./")) {
|
||||
return {
|
||||
packageRegistry: "other",
|
||||
packageName: "local-executable",
|
||||
};
|
||||
}
|
||||
|
||||
// Padrão: tratar como "other"
|
||||
return {
|
||||
packageRegistry: "other",
|
||||
packageName: command,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa instalação em background (não bloqueia resposta HTTP)
|
||||
*/
|
||||
async function installInBackground(config: InstallationConfig): Promise<void> {
|
||||
const { mcpId, enableLogs } = config;
|
||||
|
||||
try {
|
||||
console.log(`📦 Installing ${mcpId} in background (manual mode)...`);
|
||||
|
||||
// Callback de progresso
|
||||
const onProgress = (progress: InstallationProgress) => {
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
progress.status,
|
||||
progress.message
|
||||
);
|
||||
|
||||
if (enableLogs && progress.logs && progress.logs.length > 0) {
|
||||
MCPRepository.appendLogs(mcpId, progress.logs);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${mcpId}] ${progress.status}: ${progress.message} (${progress.percentage}%)`
|
||||
);
|
||||
};
|
||||
|
||||
// Instalar
|
||||
onProgress({
|
||||
status: MCPInstallationStatus.DOWNLOADING,
|
||||
message: "Preparing manual installation...",
|
||||
percentage: 5,
|
||||
});
|
||||
|
||||
const environment = await MCPInstallerService.install(config, onProgress);
|
||||
|
||||
// Salvar ambiente
|
||||
MCPRepository.saveEnvironment(mcpId, environment);
|
||||
|
||||
onProgress({
|
||||
status: MCPInstallationStatus.TESTING,
|
||||
message: "Validating MCP server...",
|
||||
percentage: 85,
|
||||
});
|
||||
|
||||
// Validar servidor MCP
|
||||
const validation = await MCPValidatorService.validateMCPServer(
|
||||
environment,
|
||||
config.config
|
||||
);
|
||||
|
||||
// Salvar resultado de validação
|
||||
MCPRepository.saveValidationResult(mcpId, validation);
|
||||
|
||||
if (validation.success) {
|
||||
// Salvar ferramentas descobertas
|
||||
MCPRepository.saveTools(mcpId, validation.tools);
|
||||
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"ready",
|
||||
`Installed successfully with ${validation.tools.length} tools`
|
||||
);
|
||||
|
||||
console.log(
|
||||
`✅ MCP ${mcpId} (manual) installed and validated successfully`
|
||||
);
|
||||
} else {
|
||||
// Validação falhou, mas instalação pode estar OK
|
||||
console.warn(
|
||||
`⚠️ MCP ${mcpId} (manual) installed but validation failed: ${validation.error}`
|
||||
);
|
||||
|
||||
// Tentar validação simples como fallback
|
||||
const simpleValidation = await MCPValidatorService.simpleValidation(
|
||||
environment,
|
||||
config.config
|
||||
);
|
||||
|
||||
if (simpleValidation.success) {
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"ready",
|
||||
"Installed (validation partial)"
|
||||
);
|
||||
console.log(
|
||||
`✅ MCP ${mcpId} (manual) installed (simple validation passed)`
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
validation.error || simpleValidation.error || "Validation failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Manual installation failed for ${mcpId}:`, error);
|
||||
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"failed",
|
||||
error.message || "Manual installation failed"
|
||||
);
|
||||
|
||||
if (enableLogs) {
|
||||
MCPRepository.appendLogs(mcpId, [
|
||||
`ERROR: ${error.message}`,
|
||||
error.stack || "",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const mcpId = searchParams.get("mcpId");
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const installation = MCPRepository.getInstalledMCP(mcpId);
|
||||
|
||||
if (!installation) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Parsear logs se existirem
|
||||
let logs: string[] = [];
|
||||
if (installation.install_logs) {
|
||||
try {
|
||||
logs = JSON.parse(installation.install_logs);
|
||||
} catch {
|
||||
logs = [installation.install_logs];
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcpId: installation.id,
|
||||
status: installation.status || "unknown",
|
||||
message: installation.status_message || "",
|
||||
logs,
|
||||
environment: installation.environment_path,
|
||||
executableCommand: installation.executable_command,
|
||||
installedAt: installation.installed_at,
|
||||
updatedAt: installation.updated_at,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error getting installation status:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to get installation status",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPInstallerService } from "@/lib/services/mcp-installer";
|
||||
import { MCPValidatorService } from "@/lib/services/mcp-validator";
|
||||
import { DeepNLPService } from "@/lib/services/deepnlp";
|
||||
import type {
|
||||
InstallationConfig,
|
||||
InstallationProgress,
|
||||
} from "@/lib/types/mcp-installer";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { mcpId, customConfig, enableLogs } = await request.json();
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🚀 Starting installation for MCP: ${mcpId}`);
|
||||
|
||||
// Verificar se já está instalado
|
||||
if (MCPRepository.isInstalled(mcpId)) {
|
||||
const existing = MCPRepository.getInstalledMCP(mcpId);
|
||||
|
||||
// Se já está instalado e pronto, retornar erro
|
||||
if (existing.status === "ready") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is already installed`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Se está em processo de instalação ou falhou, permitir reinstalação
|
||||
console.log(
|
||||
`Re-installing MCP ${mcpId} (current status: ${existing.status})`
|
||||
);
|
||||
}
|
||||
|
||||
// Obter configuração
|
||||
let serverConfig: any;
|
||||
let packageName: string;
|
||||
let packageRegistry: string | null = null;
|
||||
let repositoryUrl: string | undefined;
|
||||
|
||||
if (customConfig) {
|
||||
// Usar configuração personalizada
|
||||
console.log(`Using custom configuration for ${mcpId}`);
|
||||
serverConfig = customConfig;
|
||||
packageName = customConfig.packageName || mcpId;
|
||||
packageRegistry = customConfig.packageRegistry || null;
|
||||
repositoryUrl = customConfig.repositoryUrl;
|
||||
} else {
|
||||
// Buscar configuração da API
|
||||
console.log(`Fetching configuration for ${mcpId}`);
|
||||
|
||||
try {
|
||||
// Tentar DeepNLP primeiro
|
||||
const deepnlpConfig = await DeepNLPService.getServerConfig(mcpId);
|
||||
if (deepnlpConfig.items && deepnlpConfig.items.length > 0) {
|
||||
serverConfig = deepnlpConfig.items[0];
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`DeepNLP config not found, trying cache...`);
|
||||
}
|
||||
|
||||
if (!serverConfig) {
|
||||
throw new Error(`No configuration found for MCP ${mcpId}`);
|
||||
}
|
||||
|
||||
// Extrair informações do pacote
|
||||
packageName = serverConfig.packageName || serverConfig.name || mcpId;
|
||||
}
|
||||
|
||||
// Preparar configuração de instalação
|
||||
const installConfig: InstallationConfig = {
|
||||
mcpId,
|
||||
packageName,
|
||||
packageRegistry: packageRegistry as any,
|
||||
repositoryUrl,
|
||||
config: serverConfig,
|
||||
enableLogs: enableLogs === true,
|
||||
};
|
||||
|
||||
// Salvar como PENDING no banco
|
||||
if (!MCPRepository.isInstalled(mcpId)) {
|
||||
MCPRepository.saveInstallation(mcpId, serverConfig, []);
|
||||
}
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"pending",
|
||||
"Starting installation..."
|
||||
);
|
||||
|
||||
// Iniciar instalação em background
|
||||
installInBackground(installConfig);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Installation started",
|
||||
mcpId,
|
||||
status: "pending",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error starting installation:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to start installation",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa instalação em background (não bloqueia resposta HTTP)
|
||||
*/
|
||||
async function installInBackground(config: InstallationConfig): Promise<void> {
|
||||
const { mcpId, enableLogs } = config;
|
||||
|
||||
try {
|
||||
console.log(`📦 Installing ${mcpId} in background...`);
|
||||
|
||||
// Callback de progresso
|
||||
const onProgress = (progress: InstallationProgress) => {
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
progress.status,
|
||||
progress.message
|
||||
);
|
||||
|
||||
if (enableLogs && progress.logs && progress.logs.length > 0) {
|
||||
MCPRepository.appendLogs(mcpId, progress.logs);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${mcpId}] ${progress.status}: ${progress.message} (${progress.percentage}%)`
|
||||
);
|
||||
};
|
||||
|
||||
// Instalar
|
||||
onProgress({
|
||||
status: "downloading",
|
||||
message: "Preparing installation...",
|
||||
percentage: 5,
|
||||
});
|
||||
|
||||
const environment = await MCPInstallerService.install(config, onProgress);
|
||||
|
||||
// Salvar ambiente
|
||||
MCPRepository.saveEnvironment(mcpId, environment);
|
||||
|
||||
onProgress({
|
||||
status: "testing",
|
||||
message: "Validating MCP server...",
|
||||
percentage: 85,
|
||||
});
|
||||
|
||||
// Validar servidor MCP
|
||||
const validation = await MCPValidatorService.validateMCPServer(
|
||||
environment,
|
||||
config.config
|
||||
);
|
||||
|
||||
// Salvar resultado de validação
|
||||
MCPRepository.saveValidationResult(mcpId, validation);
|
||||
|
||||
if (validation.success) {
|
||||
// Salvar ferramentas descobertas
|
||||
MCPRepository.saveTools(mcpId, validation.tools);
|
||||
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"ready",
|
||||
`Installed successfully with ${validation.tools.length} tools`
|
||||
);
|
||||
|
||||
console.log(`✅ MCP ${mcpId} installed and validated successfully`);
|
||||
} else {
|
||||
// Validação falhou, mas instalação pode estar OK
|
||||
console.warn(
|
||||
`⚠️ MCP ${mcpId} installed but validation failed: ${validation.error}`
|
||||
);
|
||||
|
||||
// Tentar validação simples como fallback
|
||||
const simpleValidation = await MCPValidatorService.simpleValidation(
|
||||
environment,
|
||||
config.config
|
||||
);
|
||||
|
||||
if (simpleValidation.success) {
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"ready",
|
||||
"Installed (validation partial)"
|
||||
);
|
||||
console.log(`✅ MCP ${mcpId} installed (simple validation passed)`);
|
||||
} else {
|
||||
throw new Error(
|
||||
validation.error || simpleValidation.error || "Validation failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Installation failed for ${mcpId}:`, error);
|
||||
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"failed",
|
||||
error.message || "Installation failed"
|
||||
);
|
||||
|
||||
if (enableLogs) {
|
||||
MCPRepository.appendLogs(mcpId, [
|
||||
`ERROR: ${error.message}`,
|
||||
error.stack || "",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
// Marketplace cache removido
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
console.log("Fetching installed MCPs...");
|
||||
|
||||
// Buscar todos os MCPs instalados com detalhes
|
||||
const installedMcps = MCPRepository.listInstalledWithDetails();
|
||||
|
||||
// Retornar dados de instalados sem enriquecimento de marketplace
|
||||
const enrichedMcps = installedMcps.map((mcp) => ({
|
||||
...mcp,
|
||||
author: mcp.owner !== "unknown" ? mcp.owner : "Unknown",
|
||||
description: `MCP instalado: ${mcp.name}`,
|
||||
repository: "",
|
||||
homepage: "",
|
||||
rating: 0,
|
||||
totalRatings: 0,
|
||||
category: "other",
|
||||
tags: [],
|
||||
status: mcp.status || "unknown",
|
||||
statusMessage: mcp.status_message || "",
|
||||
}));
|
||||
|
||||
console.log(`Found ${enrichedMcps.length} installed MCPs`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: enrichedMcps,
|
||||
total: enrichedMcps.length,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching installed MCPs:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to fetch installed MCPs",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { DeepNLPService } from "@/lib/services/deepnlp";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const mcpId = searchParams.get("mcpId");
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Fetching server config for: ${mcpId}`);
|
||||
|
||||
// Marketplace removido: pular cache local
|
||||
|
||||
// Tentar DeepNLP
|
||||
try {
|
||||
const serverConfig = await DeepNLPService.getServerConfig(mcpId);
|
||||
|
||||
if (serverConfig.items && serverConfig.items.length > 0) {
|
||||
console.log(`Found config from DeepNLP for ${mcpId}`);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
config: serverConfig.items[0],
|
||||
source: "deepnlp",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`DeepNLP lookup failed for ${mcpId}, continuing...`);
|
||||
}
|
||||
|
||||
// Se não encontrou configuração em nenhum lugar, retornar config vazio
|
||||
console.log(`No config found for ${mcpId}, returning empty config`);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
config: null,
|
||||
source: "none",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching server config:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to fetch server config",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPValidatorService } from "@/lib/services/mcp-validator";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { mcpId } = await request.json();
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID é obrigatório" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🧪 Testing MCP server: ${mcpId}`);
|
||||
|
||||
// Buscar dados do MCP no banco
|
||||
const mcpData = MCPRepository.getInstalledMCP(mcpId);
|
||||
if (!mcpData) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP não encontrado" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Obter ambiente de instalação
|
||||
const environment = MCPRepository.getEnvironment(mcpId);
|
||||
|
||||
if (!environment) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"MCP não está instalado corretamente. Ambiente não encontrado.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Testing environment: ${environment.executable} ${environment.args?.join(
|
||||
" "
|
||||
)}`
|
||||
);
|
||||
|
||||
// Validar servidor usando protocolo JSON-RPC MCP
|
||||
const validationResult = await MCPValidatorService.validateMCPServer(
|
||||
environment,
|
||||
mcpData.config
|
||||
);
|
||||
|
||||
// Salvar resultado da validação
|
||||
MCPRepository.saveValidationResult(mcpId, validationResult);
|
||||
|
||||
if (validationResult.success) {
|
||||
// Atualizar ferramentas se validação foi bem-sucedida
|
||||
if (validationResult.tools && validationResult.tools.length > 0) {
|
||||
MCPRepository.saveTools(mcpId, validationResult.tools);
|
||||
}
|
||||
|
||||
console.log(`✅ Test successful for ${mcpId}:`, {
|
||||
protocol: validationResult.protocol,
|
||||
tools: validationResult.tools.length,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
result: {
|
||||
success: true,
|
||||
protocol: validationResult.protocol,
|
||||
toolsCount: validationResult.tools.length,
|
||||
tools: validationResult.tools,
|
||||
capabilities: validationResult.capabilities,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Se validação falhou, tentar validação simples
|
||||
console.warn(`⚠️ JSON-RPC validation failed, trying simple validation...`);
|
||||
|
||||
const simpleResult = await MCPValidatorService.simpleValidation(
|
||||
environment,
|
||||
mcpData.config,
|
||||
5000
|
||||
);
|
||||
|
||||
if (simpleResult.success) {
|
||||
console.log(`✅ Simple validation passed for ${mcpId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
result: {
|
||||
success: true,
|
||||
protocol: "unknown",
|
||||
message: "Server starts but JSON-RPC validation failed",
|
||||
toolsCount: 0,
|
||||
tools: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Ambas validações falharam
|
||||
console.error(`❌ All validations failed for ${mcpId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error:
|
||||
validationResult.error || simpleResult.error || "Validation failed",
|
||||
result: {
|
||||
success: false,
|
||||
protocol: validationResult.protocol,
|
||||
error: validationResult.error,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error testing MCP server:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPEnvironmentService } from "@/lib/services/mcp-environment";
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
const { mcpId } = await request.json();
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🗑️ Uninstalling MCP: ${mcpId}`);
|
||||
|
||||
// Verificar se está instalado
|
||||
if (!MCPRepository.isInstalled(mcpId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is not installed`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Obter ambiente antes de remover do banco
|
||||
const environment = MCPRepository.getEnvironment(mcpId);
|
||||
|
||||
// Remover do banco local
|
||||
MCPRepository.removeInstallation(mcpId);
|
||||
|
||||
console.log(`✓ MCP ${mcpId} removed from database`);
|
||||
|
||||
// Limpar arquivos de instalação se ambiente existe
|
||||
if (environment) {
|
||||
try {
|
||||
await MCPEnvironmentService.cleanupEnvironment(environment);
|
||||
console.log(`✓ MCP ${mcpId} environment cleaned up`);
|
||||
} catch (error: any) {
|
||||
console.warn(
|
||||
`⚠️ Could not clean up environment for ${mcpId}: ${error.message}`
|
||||
);
|
||||
// Não falhar a desinstalação se limpeza falhar
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `MCP ${mcpId} uninstalled successfully`,
|
||||
mcpId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error uninstalling MCP:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message || "Failed to uninstall MCP" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const { mcpId, newConfig } = await request.json();
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!newConfig) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "New configuration is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Updating config for MCP: ${mcpId}`);
|
||||
|
||||
// Verificar se o MCP está instalado
|
||||
if (!MCPRepository.isInstalled(mcpId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is not installed`,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Atualizar configuração
|
||||
MCPRepository.updateConfig(mcpId, newConfig);
|
||||
|
||||
console.log(`MCP ${mcpId} configuration updated successfully`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `MCP ${mcpId} configuration updated successfully`,
|
||||
mcpId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error updating MCP configuration:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message || "Failed to update MCP configuration" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,377 +0,0 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { chatWithStream, ensureOllamaAvailable } from "@/lib/ollama";
|
||||
import { convertToOllamaMessages } from "@/lib/chat";
|
||||
import { detectModelCapabilities } from "@/lib/services/model-capabilities";
|
||||
import { MCPExecutor } from "@/lib/services/mcp-executor";
|
||||
import {
|
||||
generateToolsPrompt,
|
||||
extractToolCallFromResponse,
|
||||
formatToolResultForContext,
|
||||
} from "@/lib/prompts/mcp-tools-prompt";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { model, messages, options, system, activeMcps } = await req.json();
|
||||
|
||||
if (!model || !messages?.length) {
|
||||
return new Response("Missing model or messages", { status: 400 });
|
||||
}
|
||||
|
||||
// Detectar capabilities do modelo
|
||||
const capabilities = detectModelCapabilities(model);
|
||||
|
||||
// Preparar tools se MCPs ativos foram fornecidos
|
||||
let tools: Array<any> = [];
|
||||
let toolsPrompt = "";
|
||||
const mcpToolsMap = new Map<string, { mcpId: string; toolName: string }>();
|
||||
|
||||
if (activeMcps && activeMcps.length > 0) {
|
||||
console.log(`🔧 Active MCPs for chat: ${activeMcps.join(", ")}`);
|
||||
|
||||
// Buscar tools de todos os MCPs ativos
|
||||
for (const mcpId of activeMcps) {
|
||||
const mcpTools = MCPExecutor.getAvailableTools(mcpId);
|
||||
|
||||
for (const tool of mcpTools) {
|
||||
const toolKey = `${mcpId}__${tool.name}`;
|
||||
|
||||
if (capabilities.supportsNativeTools) {
|
||||
// Formato OpenAI para modelos com suporte nativo
|
||||
tools.push({
|
||||
type: "function",
|
||||
function: {
|
||||
name: toolKey,
|
||||
description: tool.description,
|
||||
parameters: tool.inputSchema,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Mapa para lookup rápido
|
||||
mcpToolsMap.set(toolKey, { mcpId, toolName: tool.name });
|
||||
}
|
||||
}
|
||||
|
||||
// Se modelo não suporta native tools, usar prompt engineering
|
||||
if (!capabilities.supportsNativeTools) {
|
||||
const allTools = Array.from(mcpToolsMap.entries()).map(
|
||||
([key, { mcpId, toolName }]) => {
|
||||
const mcpTools = MCPExecutor.getAvailableTools(mcpId);
|
||||
return mcpTools.find((t) => t.name === toolName)!;
|
||||
}
|
||||
);
|
||||
toolsPrompt = generateToolsPrompt(allTools);
|
||||
}
|
||||
}
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
let isClosed = false;
|
||||
let accumulatedResponse = "";
|
||||
let toolExecuted = false;
|
||||
let lastExtractedToolCall: { name: string; parameters: any } | null =
|
||||
null;
|
||||
|
||||
const safeEnqueue = (data: string) => {
|
||||
if (!isClosed) {
|
||||
try {
|
||||
controller.enqueue(new TextEncoder().encode(data));
|
||||
} catch (error) {
|
||||
console.error("Error enqueuing data:", error);
|
||||
isClosed = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const safeClose = () => {
|
||||
if (!isClosed) {
|
||||
try {
|
||||
controller.close();
|
||||
isClosed = true;
|
||||
} catch (error) {
|
||||
console.error("Error closing controller:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Garantir disponibilidade com timeout curto (não bloquear)
|
||||
ensureOllamaAvailable({ timeoutMs: 2000 }).catch(() => {});
|
||||
const ollamaMessages = convertToOllamaMessages(messages);
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.log("🗣️ [CHAT] Messages ->", ollamaMessages);
|
||||
}
|
||||
|
||||
// Merge system prompt com tools prompt
|
||||
const finalSystemPrompt = toolsPrompt
|
||||
? `${system || ""}\n\n${toolsPrompt}`
|
||||
: system;
|
||||
if (toolsPrompt && process.env.NODE_ENV !== "production") console.log("🧩 [CHAT] Tools prompt injected");
|
||||
|
||||
// Preparar parâmetros para chat
|
||||
const chatParams = capabilities.supportsNativeTools ? { tools } : {};
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (capabilities.supportsNativeTools) {
|
||||
console.log(
|
||||
"🧰 [CHAT] Native tools enabled:",
|
||||
tools?.map((t: any) => t.function?.name)
|
||||
);
|
||||
} else {
|
||||
console.log("🧰 [CHAT] Prompt-engineering tools mode");
|
||||
}
|
||||
}
|
||||
|
||||
const response = await chatWithStream(
|
||||
model,
|
||||
ollamaMessages,
|
||||
options,
|
||||
finalSystemPrompt,
|
||||
capabilities.supportsNativeTools ? tools : undefined
|
||||
);
|
||||
if (process.env.NODE_ENV !== "production") console.log("🌊 [CHAT] Streaming started for model:", model);
|
||||
|
||||
const normalizeParameters = (params: any) => {
|
||||
try {
|
||||
if (typeof params === "string") {
|
||||
params = JSON.parse(params);
|
||||
}
|
||||
} catch {
|
||||
// ignora erro de parse e mantém original
|
||||
}
|
||||
if (params && typeof params === "object") {
|
||||
const cleaned: Record<string, any> = {};
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v && typeof v === "object" && Object.keys(v).length === 0) {
|
||||
// remove objetos vazios que costumam vir de UIs (ex.: count: {})
|
||||
continue;
|
||||
}
|
||||
cleaned[k] = v;
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
return params ?? {};
|
||||
};
|
||||
|
||||
for await (const chunk of response) {
|
||||
if (process.env.NODE_ENV !== "production") console.log("📦 [CHAT stream chunk]", JSON.stringify(chunk));
|
||||
// Verificar se há tool calls (modelos com suporte nativo)
|
||||
if (
|
||||
(chunk.message?.tool_calls &&
|
||||
chunk.message.tool_calls.length > 0) ||
|
||||
(Array.isArray((chunk as any).tool_calls) &&
|
||||
(chunk as any).tool_calls.length > 0)
|
||||
) {
|
||||
if (process.env.NODE_ENV !== "production") console.log("🔧 Model requested tool calls (native)");
|
||||
|
||||
const nativeCalls =
|
||||
chunk.message?.tool_calls || (chunk as any).tool_calls || [];
|
||||
for (const toolCall of nativeCalls) {
|
||||
const toolKey = toolCall.function.name;
|
||||
const toolInfo = mcpToolsMap.get(toolKey);
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.log("🧭 [CHAT] Native tool call:", toolKey, "->", toolInfo);
|
||||
}
|
||||
|
||||
if (toolInfo) {
|
||||
// Executar tool
|
||||
const result = await MCPExecutor.executeMCPTool(
|
||||
toolInfo.mcpId,
|
||||
toolInfo.toolName,
|
||||
normalizeParameters(
|
||||
toolCall.function.arguments ||
|
||||
toolCall.function.parameters ||
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toolExecuted = true;
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.log("✅ [CHAT] Tool executed successfully", {
|
||||
mcpId: toolInfo.mcpId,
|
||||
toolName: toolInfo.toolName,
|
||||
executionTime: result.executionTime,
|
||||
});
|
||||
}
|
||||
// Enviar indicador de execução de tool
|
||||
safeEnqueue(
|
||||
JSON.stringify({
|
||||
toolExecution: {
|
||||
mcpId: toolInfo.mcpId,
|
||||
toolName: toolInfo.toolName,
|
||||
status: "success",
|
||||
},
|
||||
}) + "\n"
|
||||
);
|
||||
|
||||
// Injetar resultado no contexto e continuar
|
||||
const resultMessage = formatToolResultForContext(
|
||||
toolInfo.toolName,
|
||||
result.result
|
||||
);
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.log(
|
||||
"🧩 [CHAT] Injecting tool result into context (length)",
|
||||
String(resultMessage).length
|
||||
);
|
||||
}
|
||||
|
||||
// Fazer nova chamada ao modelo com o resultado
|
||||
const updatedMessages = [
|
||||
...ollamaMessages,
|
||||
{
|
||||
role: "assistant",
|
||||
content: chunk.message.content || "",
|
||||
},
|
||||
{ role: "system", content: resultMessage },
|
||||
];
|
||||
|
||||
const followUpResponse = await chatWithStream(
|
||||
model,
|
||||
updatedMessages,
|
||||
options,
|
||||
finalSystemPrompt,
|
||||
capabilities.supportsNativeTools ? tools : undefined
|
||||
);
|
||||
|
||||
for await (const followUpChunk of followUpResponse) {
|
||||
const token = followUpChunk.message?.content || "";
|
||||
if (token) {
|
||||
safeEnqueue(JSON.stringify({ token }) + "\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error("Tool execution failed:", result.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Streaming normal de tokens
|
||||
const token = chunk.message?.content || "";
|
||||
if (token) {
|
||||
accumulatedResponse += token;
|
||||
safeEnqueue(JSON.stringify({ token }) + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se resposta contém tool call via prompt engineering (fallback também para nativo)
|
||||
if (accumulatedResponse) {
|
||||
console.log(
|
||||
"🧾 [CHAT] Accumulated response length:",
|
||||
accumulatedResponse.length
|
||||
);
|
||||
console.log(
|
||||
"🧾 [CHAT] Accumulated preview:",
|
||||
accumulatedResponse.slice(0, 400)
|
||||
);
|
||||
const extractedToolCall =
|
||||
extractToolCallFromResponse(accumulatedResponse);
|
||||
|
||||
if (extractedToolCall) {
|
||||
if (process.env.NODE_ENV !== "production") console.log("🔧 Model requested tool call (prompt engineering)");
|
||||
lastExtractedToolCall = extractedToolCall;
|
||||
if (process.env.NODE_ENV !== "production") console.log("🧭 [CHAT] Extracted tool call:", extractedToolCall);
|
||||
|
||||
// Encontrar MCP e tool
|
||||
let foundToolInfo: { mcpId: string; toolName: string } | null =
|
||||
null;
|
||||
|
||||
for (const [key, info] of mcpToolsMap.entries()) {
|
||||
if (
|
||||
extractedToolCall.name === info.toolName ||
|
||||
extractedToolCall.name === key
|
||||
) {
|
||||
foundToolInfo = info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundToolInfo) {
|
||||
// Executar tool
|
||||
const result = await MCPExecutor.executeMCPTool(
|
||||
foundToolInfo.mcpId,
|
||||
foundToolInfo.toolName,
|
||||
normalizeParameters(extractedToolCall.parameters)
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toolExecuted = true;
|
||||
// Limpar resposta anterior (que continha o JSON)
|
||||
safeEnqueue(JSON.stringify({ clearPrevious: true }) + "\n");
|
||||
|
||||
// Enviar indicador de execução
|
||||
safeEnqueue(
|
||||
JSON.stringify({
|
||||
toolExecution: {
|
||||
mcpId: foundToolInfo.mcpId,
|
||||
toolName: foundToolInfo.toolName,
|
||||
status: "success",
|
||||
},
|
||||
}) + "\n"
|
||||
);
|
||||
|
||||
// Fazer nova chamada com resultado
|
||||
const resultMessage = formatToolResultForContext(
|
||||
foundToolInfo.toolName,
|
||||
result.result
|
||||
);
|
||||
|
||||
const updatedMessages = [
|
||||
...ollamaMessages,
|
||||
{
|
||||
role: "assistant",
|
||||
content: JSON.stringify({
|
||||
tool_call: extractedToolCall,
|
||||
}),
|
||||
},
|
||||
{ role: "system", content: resultMessage },
|
||||
];
|
||||
|
||||
const followUpResponse = await chatWithStream(
|
||||
model,
|
||||
updatedMessages,
|
||||
options,
|
||||
finalSystemPrompt
|
||||
);
|
||||
|
||||
for await (const followUpChunk of followUpResponse) {
|
||||
const token = followUpChunk.message?.content || "";
|
||||
if (token) {
|
||||
safeEnqueue(JSON.stringify({ token }) + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (process.env.NODE_ENV !== "production")
|
||||
console.log("🕵️ [CHAT] No tool call extracted from accumulated response");
|
||||
}
|
||||
}
|
||||
|
||||
safeEnqueue(JSON.stringify({ done: true }) + "\n");
|
||||
safeClose();
|
||||
} catch (error) {
|
||||
console.error("Chat streaming error:", error);
|
||||
safeEnqueue(
|
||||
JSON.stringify({ error: "Failed to generate response" }) + "\n"
|
||||
);
|
||||
safeClose();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Chat API error:", error);
|
||||
return new Response("Internal server error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { deleteModel } from "@/lib/ollama";
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { modelName } = await request.json();
|
||||
|
||||
if (!modelName) {
|
||||
return NextResponse.json(
|
||||
{ error: "Model name is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await deleteModel(modelName);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Model ${modelName} deleted successfully`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting model:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to delete model" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { listModelsViaSdk, listModelsViaHttp } from "@/lib/ollama";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
try {
|
||||
await listModelsViaSdk();
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch {
|
||||
await listModelsViaHttp();
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false });
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { listModelsViaSdk, listModelsViaHttp, ensureOllamaAvailable } from "@/lib/ollama";
|
||||
import {
|
||||
estimateCpuVram,
|
||||
inferDevice,
|
||||
inferQuantization,
|
||||
mockModels,
|
||||
type ModelInfo,
|
||||
} from "@/lib/models";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// Cache leve em memória para reduzir chamadas repetidas em curto intervalo
|
||||
let __modelsCache: { ts: number; payload: any } | null = null;
|
||||
const CACHE_TTL_MS = 8000; // 8s
|
||||
|
||||
function normalizeModel(model: any): ModelInfo {
|
||||
const name: string = model?.model || model?.name || "unknown";
|
||||
const sizeBytes: number = model?.size || 0;
|
||||
const sizeGB = Number(Math.max(0.1, sizeBytes / 1024 ** 3).toFixed(2));
|
||||
const quantization = inferQuantization(name);
|
||||
const device = inferDevice(model?.details || name);
|
||||
const { cpuPct, vramGB } = estimateCpuVram({ sizeGB, quantization, device });
|
||||
return {
|
||||
id: name,
|
||||
name,
|
||||
sizeGB,
|
||||
quantization,
|
||||
device,
|
||||
estCpuUsagePct: cpuPct,
|
||||
estVramGB: vramGB,
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Tentar retorno via cache recente
|
||||
if (__modelsCache && Date.now() - __modelsCache.ts < CACHE_TTL_MS) {
|
||||
return NextResponse.json(__modelsCache.payload);
|
||||
}
|
||||
|
||||
let models: any[] = [];
|
||||
// Tentar garantir disponibilidade rapidamente (não bloquear longamente a UI)
|
||||
await ensureOllamaAvailable({ timeoutMs: 3000 }).catch(() => {});
|
||||
try {
|
||||
models = await listModelsViaSdk();
|
||||
} catch {
|
||||
models = await listModelsViaHttp();
|
||||
}
|
||||
const normalized = models.map((m) => normalizeModel(m));
|
||||
const payload = { offline: false, models: normalized };
|
||||
__modelsCache = { ts: Date.now(), payload };
|
||||
return NextResponse.json(payload);
|
||||
} catch (e) {
|
||||
const payload = { offline: true, models: mockModels };
|
||||
__modelsCache = { ts: Date.now(), payload };
|
||||
return NextResponse.json(payload);
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { pullModel, ensureOllamaAvailable } from "@/lib/ollama";
|
||||
import { getCatalogModels } from "@/lib/catalog";
|
||||
import { addSavedRemoteModel } from "@/lib/user-remote-models";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { model } = await req.json();
|
||||
|
||||
if (!model) {
|
||||
return new Response("Missing model", { status: 400 });
|
||||
}
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
// Garantir que o servidor esteja disponível antes do pull
|
||||
const ok = await ensureOllamaAvailable({ timeoutMs: 20000 });
|
||||
if (!ok) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
JSON.stringify({ error: "Ollama indisponível" }) + "\n",
|
||||
),
|
||||
);
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar metadados do catálogo para melhorar mensagens de erro
|
||||
const baseName = String(model).split(":")[0];
|
||||
const catalog = await getCatalogModels();
|
||||
const catalogEntry = catalog.find((m) => m.name === baseName);
|
||||
|
||||
const response = await pullModel(model);
|
||||
|
||||
for await (const chunk of response) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(JSON.stringify(chunk) + "\n"),
|
||||
);
|
||||
}
|
||||
|
||||
// Persistir referência do modelo baixado para reutilização futura
|
||||
try {
|
||||
await addSavedRemoteModel({ name: String(model), installed: true });
|
||||
} catch {}
|
||||
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
JSON.stringify({ status: "success" }) + "\n",
|
||||
),
|
||||
);
|
||||
controller.close();
|
||||
} catch (error: any) {
|
||||
console.error("Pull streaming error:", error);
|
||||
const message = typeof error?.message === "string" ? error.message : "Failed to pull model";
|
||||
const baseName = String(model).split(":")[0];
|
||||
let hint: string | undefined;
|
||||
|
||||
// Recarregar entrada de catálogo para enriquecer erro
|
||||
let catalogEntry: { url?: string; tags_count?: number } | undefined;
|
||||
try {
|
||||
const catalog = await getCatalogModels();
|
||||
catalogEntry = catalog.find((m) => m.name === baseName);
|
||||
} catch {}
|
||||
|
||||
// Heurísticas: manifest não encontrado → provavelmente falta tag válida
|
||||
if (/manifest/i.test(message) || /does not exist/i.test(message)) {
|
||||
hint = "Modelo/tag não encontrado no registro. Selecione uma tag válida ou verifique a página do modelo.";
|
||||
}
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
error: "Failed to pull model",
|
||||
detail: message,
|
||||
model,
|
||||
name: baseName,
|
||||
};
|
||||
|
||||
// Anexar URL do catálogo quando disponível
|
||||
if (catalogEntry?.url) {
|
||||
payload.url = catalogEntry.url;
|
||||
}
|
||||
|
||||
// Se usuário não especificou tag (sem ":") e sabemos que há múltiplas tags
|
||||
if (!String(model).includes(":") && typeof catalogEntry?.tags_count === "number") {
|
||||
payload.tags_count = catalogEntry.tags_count;
|
||||
if (!hint && catalogEntry.tags_count > 0) {
|
||||
hint = "Este modelo pode não ter a tag 'latest'. Escolha uma tag específica.";
|
||||
}
|
||||
}
|
||||
|
||||
if (hint) payload.hint = hint;
|
||||
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(JSON.stringify(payload) + "\n"),
|
||||
);
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Pull API error:", error);
|
||||
return new Response("Internal server error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getSavedRemoteModels } from "@/lib/user-remote-models";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const query = searchParams.get("q") || undefined;
|
||||
|
||||
const saved = await getSavedRemoteModels();
|
||||
const q = (query || "").trim().toLowerCase();
|
||||
const filtered = q
|
||||
? saved.filter((m) => m.name.toLowerCase().includes(q))
|
||||
: saved;
|
||||
|
||||
const models = filtered.map((m) => ({
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
url: m.url,
|
||||
tags_count: m.tags_count,
|
||||
installed: m.installed === true,
|
||||
remote: true,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ success: true, models });
|
||||
} catch (error) {
|
||||
console.error("Error searching remote models:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to search remote models" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getCatalogModels } from "@/lib/catalog";
|
||||
|
||||
// Cache simples em memória para evitar múltiplos scrapes por nome
|
||||
const TAGS_CACHE = new Map<string, { tags: string[]; expiresAt: number }>();
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000; // 1h
|
||||
|
||||
async function fetchTagsFromPage(url: string): Promise<string[]> {
|
||||
try {
|
||||
const res = await fetch(url, { cache: "no-store" });
|
||||
if (!res.ok) return [];
|
||||
const html = await res.text();
|
||||
|
||||
// Heurística best-effort: procurar padrões de tag na página
|
||||
// Exemplos possíveis: ":7b", ":8b", ":q4_K_S", etc.
|
||||
// Evitar listas enormes ou falsas-positivas: limitar tamanho e caracteres
|
||||
const tagRegex = /:\s*([A-Za-z0-9._-]{1,32})/g;
|
||||
const found = new Set<string>();
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = tagRegex.exec(html)) !== null) {
|
||||
const tag = match[1];
|
||||
if (tag && !/^(http|https)$/i.test(tag)) {
|
||||
found.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Retornar até 50 tags para não sobrecarregar UI
|
||||
return Array.from(found).slice(0, 50);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const name = (searchParams.get("name") || "").trim();
|
||||
if (!name) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing name" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Cache
|
||||
const cached = TAGS_CACHE.get(name);
|
||||
const now = Date.now();
|
||||
if (cached && cached.expiresAt > now) {
|
||||
return NextResponse.json({ success: true, name, tags: cached.tags });
|
||||
}
|
||||
|
||||
const catalog = await getCatalogModels();
|
||||
const entry = catalog.find((m) => m.name === name);
|
||||
const url = entry?.url;
|
||||
|
||||
// Sem URL conhecida → não conseguimos buscar tags no registro
|
||||
if (!url) {
|
||||
TAGS_CACHE.set(name, { tags: [], expiresAt: now + CACHE_TTL_MS });
|
||||
return NextResponse.json({ success: true, name, tags: [] });
|
||||
}
|
||||
|
||||
const tags = await fetchTagsFromPage(url);
|
||||
TAGS_CACHE.set(name, { tags, expiresAt: now + CACHE_TTL_MS });
|
||||
return NextResponse.json({ success: true, name, url, tags });
|
||||
} catch (error) {
|
||||
console.error("Error fetching model tags:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch tags" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { Ollama } from "ollama";
|
||||
import { OLLAMA_HOST } from "@/lib/ollama";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const model = url.searchParams.get("model");
|
||||
if (!model) {
|
||||
return NextResponse.json({ error: "model required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const client = new Ollama({ host: OLLAMA_HOST });
|
||||
await client.chat({
|
||||
model,
|
||||
messages: [{ role: "system", content: "warm" }],
|
||||
stream: false,
|
||||
options: { keep_alive: "5m" },
|
||||
});
|
||||
|
||||
return NextResponse.json({ warmed: true });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ warmed: false }, { status: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPreferences, setPreferences } from "@/lib/preferences";
|
||||
|
||||
// Estrutura de preferências
|
||||
type UserPreferences = {
|
||||
selectedModel: string | null;
|
||||
systemPrompt: string;
|
||||
device: "auto" | "cpu" | "gpu";
|
||||
gpuIndex: number;
|
||||
numGpu: number;
|
||||
};
|
||||
|
||||
// GET - Buscar preferências
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = "default"; // Pode ser expandido para multi-usuário
|
||||
const prefs = await getPreferences(userId);
|
||||
return NextResponse.json(prefs);
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar preferências:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro interno do servidor" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Salvar preferências
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = "default";
|
||||
const prefs = await request.json();
|
||||
await setPreferences(userId, prefs);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Erro ao salvar preferências:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro interno do servidor" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { semanticSearch } from "@/lib/vector-search";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { query, limit = 5 } = await request.json();
|
||||
|
||||
if (!query) {
|
||||
return NextResponse.json(
|
||||
{ error: "Query é obrigatória" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const results = await semanticSearch(query, limit);
|
||||
|
||||
return NextResponse.json(results);
|
||||
} catch (error) {
|
||||
console.error("Erro na busca semântica:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro interno do servidor" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import os from "os";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
const usedMem = totalMem - freeMem;
|
||||
const load = os.loadavg();
|
||||
const memUsage = process.memoryUsage();
|
||||
|
||||
// Base com os/fallback
|
||||
const baseInfo: any = {
|
||||
os: {
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
release: os.release(),
|
||||
uptimeSec: os.uptime(),
|
||||
},
|
||||
cpu: {
|
||||
model: os.cpus()?.[0]?.model || "unknown",
|
||||
cores: os.cpus()?.length || 0,
|
||||
speed: os.cpus()?.[0]?.speed || 0,
|
||||
},
|
||||
memory: {
|
||||
total: totalMem,
|
||||
free: freeMem,
|
||||
used: usedMem,
|
||||
modules: [] as Array<{ size: number; clockSpeed?: number; type?: string }>,
|
||||
},
|
||||
gpus: [] as Array<{ index: number; name: string; vendor?: string; vramMB?: number; bus?: string }>,
|
||||
metrics: {
|
||||
loadAvg: { "1m": load[0] ?? 0, "5m": load[1] ?? 0, "15m": load[2] ?? 0 },
|
||||
process: {
|
||||
rss: memUsage.rss,
|
||||
heapUsed: memUsage.heapUsed,
|
||||
heapTotal: memUsage.heapTotal,
|
||||
external: memUsage.external,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Tentar usar systeminformation, se disponível
|
||||
try {
|
||||
// Import dinâmico sem acionar o bundler (evita Module not found)
|
||||
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
||||
const dynImport: (m: string) => Promise<any> = new Function(
|
||||
"m",
|
||||
"return import(m)"
|
||||
) as any;
|
||||
const si = await dynImport("systeminformation");
|
||||
|
||||
const [cpu, osInfo, memLayout, graphics, currentLoad] = await Promise.all([
|
||||
si.cpu(),
|
||||
si.osInfo(),
|
||||
si.memLayout(),
|
||||
si.graphics(),
|
||||
si.currentLoad(),
|
||||
]);
|
||||
|
||||
baseInfo.os = {
|
||||
platform: osInfo.platform || baseInfo.os.platform,
|
||||
arch: os.arch(),
|
||||
release: osInfo.release || baseInfo.os.release,
|
||||
uptimeSec: baseInfo.os.uptimeSec,
|
||||
distro: osInfo.distro,
|
||||
};
|
||||
baseInfo.cpu = {
|
||||
model: cpu.brand || baseInfo.cpu.model,
|
||||
cores: cpu.cores || baseInfo.cpu.cores,
|
||||
physicalCores: cpu.physicalCores,
|
||||
speed: Number(cpu.speed) || baseInfo.cpu.speed,
|
||||
};
|
||||
baseInfo.memory.modules = (memLayout || []).map((m: any) => ({ size: m.size, clockSpeed: m.clockSpeed, type: m.type }));
|
||||
baseInfo.gpus = (graphics?.controllers || []).map((c: any, idx: number) => ({ index: idx, name: c.model || c.vendor || "GPU", vendor: c.vendor, vramMB: c.vram, bus: c.bus }));
|
||||
baseInfo.metrics.loadAvg = {
|
||||
"1m": currentLoad?.avgload ?? baseInfo.metrics.loadAvg["1m"],
|
||||
"5m": baseInfo.metrics.loadAvg["5m"],
|
||||
"15m": baseInfo.metrics.loadAvg["15m"],
|
||||
};
|
||||
} catch {
|
||||
// sem systeminformation, seguimos com fallback
|
||||
}
|
||||
|
||||
// Fallback adicional para detectar GPUs no Windows via PowerShell
|
||||
try {
|
||||
if ((!baseInfo.gpus || baseInfo.gpus.length === 0) && (baseInfo.os.platform === "win32" || os.platform() === "win32")) {
|
||||
const { execFileSync } = await import("child_process");
|
||||
const out = execFileSync("powershell", [
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,PNPDeviceID | ConvertTo-Json -Depth 3"
|
||||
], { encoding: "utf8", timeout: 2000 });
|
||||
const parsed = JSON.parse(out);
|
||||
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
||||
baseInfo.gpus = arr.map((g: any, idx: number) => ({ index: idx, name: g.Name || "GPU", vendor: undefined, vramMB: g.AdapterRAM ? Math.round(g.AdapterRAM / 1024 / 1024) : undefined, bus: g.PNPDeviceID }));
|
||||
}
|
||||
} catch {
|
||||
// ignore fallback errors
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, info: baseInfo });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ success: false, error: String(e) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
-164
@@ -1,164 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #0b0c0f;
|
||||
--accent: #2563eb; /* blue-600 */
|
||||
--accent-foreground: #ffffff;
|
||||
--border: #e5e7ef;
|
||||
--surface: #f6f7fb;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-border: var(--border);
|
||||
--font-sans: var(--font-inter);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #0b0c0f;
|
||||
--foreground: #e6e8ef;
|
||||
--accent: #3b82f6; /* blue-500 */
|
||||
--accent-foreground: #0b0c0f;
|
||||
--border: #2a2f3a;
|
||||
--surface: #12141a;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-inter), ui-sans-serif, system-ui;
|
||||
}
|
||||
|
||||
/* Custom scrollbar para o seletor de modelos */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--foreground);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* MCP Marketplace Styles */
|
||||
.mcp-card {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.mcp-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mcp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mcp-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-modal {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.mcp-category-badge {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.mcp-category-badge:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.mcp-rating-star {
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.mcp-rating-star:hover {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.mcp-install-button {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.mcp-install-button:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.mcp-install-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Loading animation for MCP cards */
|
||||
.mcp-loading {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* MCP detail modal animations */
|
||||
.mcp-detail-modal {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* MCP search bar focus effects */
|
||||
.mcp-search-input:focus {
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* MCP category filter active state */
|
||||
.mcp-category-active {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary));
|
||||
color: var(--primary-foreground);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* MCP tag hover effects */
|
||||
.mcp-tag {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.mcp-tag:hover {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "../components/theme-provider";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${inter.variable} antialiased`}>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ModeToggle } from "@/components/mode-toggle";
|
||||
import { ChatContainer } from "@/components/chat/ChatContainer";
|
||||
import { ClientHome } from "@/components/ClientHome";
|
||||
|
||||
async function fetchModels() {
|
||||
try {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
|
||||
const res = await fetch(`${baseUrl}/api/ollama/models`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) return { offline: true, models: [] };
|
||||
return res.json();
|
||||
} catch {
|
||||
return { offline: true, models: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Home() {
|
||||
const { offline, models } = await fetchModels();
|
||||
return <ClientHome offline={offline} models={models} />;
|
||||
}
|
||||
Reference in new issue
Block a user