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
Vendored
-173
@@ -1,173 +0,0 @@
|
||||
import { DeepNLPSearchParams } from "@/lib/types/mcp";
|
||||
|
||||
interface CacheEntry {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
export class MCPCache {
|
||||
private static cache = new Map<string, CacheEntry>();
|
||||
private static TTL = 5 * 60 * 1000; // 5 minutos em millisegundos
|
||||
|
||||
/**
|
||||
* Obter dados do cache
|
||||
*/
|
||||
static get(key: string): any | null {
|
||||
const entry = this.cache.get(key);
|
||||
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verificar se expirou
|
||||
const now = Date.now();
|
||||
if (now - entry.timestamp > entry.ttl) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`Cache hit for key: ${key}`);
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Salvar dados no cache
|
||||
*/
|
||||
static set(key: string, value: any, ttl?: number): void {
|
||||
const entry: CacheEntry = {
|
||||
data: value,
|
||||
timestamp: Date.now(),
|
||||
ttl: ttl || this.TTL
|
||||
};
|
||||
|
||||
this.cache.set(key, entry);
|
||||
console.log(`Cache set for key: ${key}, TTL: ${entry.ttl}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpar cache
|
||||
*/
|
||||
static clear(): void {
|
||||
this.cache.clear();
|
||||
console.log("Cache cleared");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remover entrada específica do cache
|
||||
*/
|
||||
static delete(key: string): void {
|
||||
this.cache.delete(key);
|
||||
console.log(`Cache entry deleted for key: ${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar chave de cache baseada nos parâmetros de busca
|
||||
*/
|
||||
static generateKey(params: DeepNLPSearchParams): string {
|
||||
const sortedParams = Object.keys(params)
|
||||
.sort()
|
||||
.reduce((result, key) => {
|
||||
const value = params[key as keyof DeepNLPSearchParams];
|
||||
if (value !== undefined && value !== null) {
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
return `mcp_search_${JSON.stringify(sortedParams)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar chave para configuração de servidor
|
||||
*/
|
||||
static generateServerConfigKey(mcpId: string): string {
|
||||
return `mcp_config_${mcpId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar chave para ferramentas de servidor
|
||||
*/
|
||||
static generateServerToolsKey(mcpId: string): string {
|
||||
return `mcp_tools_${mcpId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter estatísticas do cache
|
||||
*/
|
||||
static getStats(): {
|
||||
size: number;
|
||||
entries: Array<{ key: string; age: number; ttl: number }>;
|
||||
} {
|
||||
const now = Date.now();
|
||||
const entries = Array.from(this.cache.entries()).map(([key, entry]) => ({
|
||||
key,
|
||||
age: now - entry.timestamp,
|
||||
ttl: entry.ttl
|
||||
}));
|
||||
|
||||
return {
|
||||
size: this.cache.size,
|
||||
entries
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpar entradas expiradas
|
||||
*/
|
||||
static cleanup(): number {
|
||||
const now = Date.now();
|
||||
let cleaned = 0;
|
||||
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
if (now - entry.timestamp > entry.ttl) {
|
||||
this.cache.delete(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned > 0) {
|
||||
console.log(`Cache cleanup: removed ${cleaned} expired entries`);
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se uma chave existe no cache (sem verificar TTL)
|
||||
*/
|
||||
static has(key: string): boolean {
|
||||
return this.cache.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter todas as chaves do cache
|
||||
*/
|
||||
static keys(): string[] {
|
||||
return Array.from(this.cache.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Definir TTL personalizado para diferentes tipos de dados
|
||||
*/
|
||||
static getTTLForDataType(dataType: 'search' | 'config' | 'tools'): number {
|
||||
switch (dataType) {
|
||||
case 'search':
|
||||
return 5 * 60 * 1000; // 5 minutos para buscas
|
||||
case 'config':
|
||||
return 30 * 60 * 1000; // 30 minutos para configurações
|
||||
case 'tools':
|
||||
return 15 * 60 * 1000; // 15 minutos para ferramentas
|
||||
default:
|
||||
return this.TTL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache com TTL específico para tipo de dados
|
||||
*/
|
||||
static setWithDataType(key: string, value: any, dataType: 'search' | 'config' | 'tools'): void {
|
||||
const ttl = this.getTTLForDataType(dataType);
|
||||
this.set(key, value, ttl);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
type CatalogModel = {
|
||||
name: string;
|
||||
description?: string;
|
||||
capabilities?: string[];
|
||||
parameter_sizes?: string[];
|
||||
pulls?: string;
|
||||
tags_count?: number;
|
||||
last_updated?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
type CatalogFile = {
|
||||
models?: CatalogModel[];
|
||||
};
|
||||
|
||||
let cachedModels: CatalogModel[] | null = null;
|
||||
let cachedMtimeMs: number | null = null;
|
||||
|
||||
function resolveCatalogPath(): string {
|
||||
// Arquivo fica em data/models.json na raiz do projeto
|
||||
return path.join(process.cwd(), "data", "models.json");
|
||||
}
|
||||
|
||||
export async function getCatalogModels(): Promise<CatalogModel[]> {
|
||||
try {
|
||||
const catalogPath = resolveCatalogPath();
|
||||
const stat = await fs.stat(catalogPath);
|
||||
|
||||
// Cache simples por mtime
|
||||
if (cachedModels && cachedMtimeMs && stat.mtimeMs === cachedMtimeMs) {
|
||||
return cachedModels;
|
||||
}
|
||||
|
||||
const raw = await fs.readFile(catalogPath, "utf-8");
|
||||
const json = JSON.parse(raw) as CatalogFile;
|
||||
const models = Array.isArray(json.models) ? json.models : [];
|
||||
|
||||
cachedModels = models;
|
||||
cachedMtimeMs = stat.mtimeMs;
|
||||
return models;
|
||||
} catch (error) {
|
||||
// Em caso de erro, retorne array vazio para não quebrar a UI
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
export type ChatRole = "user" | "assistant" | "system";
|
||||
|
||||
export type Message = {
|
||||
id: string;
|
||||
role: ChatRole;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type ChatState = {
|
||||
messages: Message[];
|
||||
selectedModel: string | null;
|
||||
isStreaming: boolean;
|
||||
streamingContent: string;
|
||||
};
|
||||
|
||||
export function createMessage(role: ChatRole, content: string): Message {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
role,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatTimestamp(timestamp: number): string {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
|
||||
if (minutes < 1) return "agora";
|
||||
if (minutes < 60) return `${minutes} min atrás`;
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h atrás`;
|
||||
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d atrás`;
|
||||
}
|
||||
|
||||
export function convertToOllamaMessages(messages: Message[]) {
|
||||
return messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
}));
|
||||
}
|
||||
|
||||
export function createSystemMessage(content: string): Message {
|
||||
return createMessage("system", content);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Configurações para instalação e gerenciamento de servidores MCP
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
|
||||
export const MCP_CONFIG = {
|
||||
// Diretório base onde os servidores MCP serão instalados
|
||||
serversDir:
|
||||
process.env.MCP_SERVERS_DIR || path.join(process.cwd(), "mcp-servers"),
|
||||
|
||||
// Timeout para instalação de pacotes (5 minutos)
|
||||
installTimeout: parseInt(process.env.MCP_INSTALL_TIMEOUT || "300000", 10),
|
||||
|
||||
// Timeout para testes de servidor (10 segundos)
|
||||
testTimeout: parseInt(process.env.MCP_TEST_TIMEOUT || "10000", 10),
|
||||
|
||||
// Habilitar logs detalhados (para debug/testes)
|
||||
enableLogs: process.env.MCP_ENABLE_LOGS === "true",
|
||||
|
||||
// Diretórios por tipo de pacote
|
||||
get npmDir() {
|
||||
return path.join(this.serversDir, "npm");
|
||||
},
|
||||
|
||||
get pythonDir() {
|
||||
return path.join(this.serversDir, "python");
|
||||
},
|
||||
|
||||
get rustDir() {
|
||||
return path.join(this.serversDir, "rust");
|
||||
},
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
import { Ollama } from "ollama";
|
||||
|
||||
const ollama = new Ollama({
|
||||
host: process.env.OLLAMA_HOST || "http://localhost:11434",
|
||||
});
|
||||
|
||||
export async function generateEmbedding(
|
||||
text: string,
|
||||
model: string = "nomic-embed-text"
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
// Verificar se deve forçar uso de CPU (útil quando GPU tem problemas de memória)
|
||||
const forceCpu =
|
||||
process.env.OLLAMA_NO_GPU === "1" || process.env.OLLAMA_NUM_GPU === "0";
|
||||
|
||||
const response = await ollama.embeddings({
|
||||
model,
|
||||
prompt: text,
|
||||
options: forceCpu ? { num_gpu: 0 } : undefined,
|
||||
});
|
||||
|
||||
return new Float32Array(response.embedding);
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao gerar embedding:", error);
|
||||
|
||||
// Se erro de GPU e ainda não tentou CPU, tentar forçar CPU
|
||||
const isGpuError =
|
||||
error?.message?.includes("CUDA") ||
|
||||
error?.message?.includes("unable to allocate") ||
|
||||
error?.status_code === 500;
|
||||
|
||||
if (
|
||||
isGpuError &&
|
||||
process.env.OLLAMA_NO_GPU !== "1" &&
|
||||
process.env.OLLAMA_NUM_GPU !== "0"
|
||||
) {
|
||||
console.warn("Erro de GPU detectado, tentando usar CPU...");
|
||||
try {
|
||||
const response = await ollama.embeddings({
|
||||
model,
|
||||
prompt: text,
|
||||
options: { num_gpu: 0 },
|
||||
});
|
||||
return new Float32Array(response.embedding);
|
||||
} catch (cpuError) {
|
||||
console.error("Erro mesmo usando CPU:", cpuError);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: retornar vetor zero se modelo não estiver disponível
|
||||
console.warn(
|
||||
`Modelo ${model} não disponível, usando vetor zero como fallback`
|
||||
);
|
||||
return new Float32Array(768); // 768 dimensões para nomic-embed-text
|
||||
}
|
||||
}
|
||||
|
||||
const EMOJI_CATEGORIES = {
|
||||
code: ["💻", "🖥️", "⌨️", "🔧", "🛠️"],
|
||||
idea: ["💡", "🧠", "💭", "🤔", "✨"],
|
||||
question: ["❓", "🤷", "🔍", "🧐", "📚"],
|
||||
creative: ["🎨", "🎭", "🎪", "🌈", "🎬"],
|
||||
business: ["💼", "📊", "💰", "📈", "🏢"],
|
||||
science: ["🔬", "🧪", "🔭", "⚗️", "🧬"],
|
||||
education: ["📖", "📝", "🎓", "✏️", "📚"],
|
||||
communication: ["💬", "📱", "📧", "🗣️", "💌"],
|
||||
general: ["🌟", "🎯", "🚀", "⚡", "🔥"],
|
||||
};
|
||||
|
||||
export async function generateChatTitle(firstMessage: string): Promise<string> {
|
||||
try {
|
||||
const response = await ollama.generate({
|
||||
model: "qwen2.5:0.5b",
|
||||
prompt: `Analise esta mensagem e crie um título curto e descritivo (MAX 30 caracteres, sem emoji):
|
||||
|
||||
Mensagem: "${firstMessage}"
|
||||
|
||||
Título (direto, sem explicação):`,
|
||||
options: {
|
||||
temperature: 0.7,
|
||||
num_predict: 15,
|
||||
},
|
||||
});
|
||||
|
||||
let title = response.response
|
||||
.trim()
|
||||
.replace(/['"]/g, "")
|
||||
.replace(/^Título:\s*/i, "")
|
||||
.substring(0, 30);
|
||||
|
||||
// Selecionar emoji baseado no conteúdo
|
||||
const emoji = selectEmoji(firstMessage);
|
||||
|
||||
return `${emoji} ${title}`;
|
||||
} catch (error) {
|
||||
console.error("Erro ao gerar título:", error);
|
||||
// Fallback: usar título simples baseado no conteúdo
|
||||
const emoji = selectEmoji(firstMessage);
|
||||
const fallbackTitle = firstMessage.substring(0, 20).trim();
|
||||
return `${emoji} ${fallbackTitle}`;
|
||||
}
|
||||
}
|
||||
|
||||
function selectEmoji(content: string): string {
|
||||
const lower = content.toLowerCase();
|
||||
|
||||
if (/code|program|debug|function|class/.test(lower))
|
||||
return EMOJI_CATEGORIES.code[
|
||||
Math.floor(Math.random() * EMOJI_CATEGORIES.code.length)
|
||||
];
|
||||
if (/why|how|what|when|where/.test(lower))
|
||||
return EMOJI_CATEGORIES.question[
|
||||
Math.floor(Math.random() * EMOJI_CATEGORIES.question.length)
|
||||
];
|
||||
if (/idea|think|create|design/.test(lower))
|
||||
return EMOJI_CATEGORIES.idea[
|
||||
Math.floor(Math.random() * EMOJI_CATEGORIES.idea.length)
|
||||
];
|
||||
if (/business|money|sale|market/.test(lower))
|
||||
return EMOJI_CATEGORIES.business[
|
||||
Math.floor(Math.random() * EMOJI_CATEGORIES.business.length)
|
||||
];
|
||||
if (/science|research|experiment/.test(lower))
|
||||
return EMOJI_CATEGORIES.science[
|
||||
Math.floor(Math.random() * EMOJI_CATEGORIES.science.length)
|
||||
];
|
||||
|
||||
return EMOJI_CATEGORIES.general[
|
||||
Math.floor(Math.random() * EMOJI_CATEGORIES.general.length)
|
||||
];
|
||||
}
|
||||
Whitespace-only changes.
-100
@@ -1,100 +0,0 @@
|
||||
export type Device = "CPU" | "GPU";
|
||||
|
||||
export type ModelInfo = {
|
||||
id: string;
|
||||
name: string;
|
||||
sizeGB: number;
|
||||
quantization: string;
|
||||
device: Device;
|
||||
estCpuUsagePct?: number;
|
||||
estVramGB?: number;
|
||||
};
|
||||
|
||||
export const mockModels: ModelInfo[] = [
|
||||
{
|
||||
id: "llama3.1-8b-q4",
|
||||
name: "llama3.1:8b-q4",
|
||||
sizeGB: 4.8,
|
||||
quantization: "Q4",
|
||||
device: "CPU",
|
||||
estCpuUsagePct: 85,
|
||||
estVramGB: 0.0,
|
||||
},
|
||||
{
|
||||
id: "llama3.1-8b-q8",
|
||||
name: "llama3.1:8b-q8",
|
||||
sizeGB: 8.9,
|
||||
quantization: "Q8",
|
||||
device: "GPU",
|
||||
estCpuUsagePct: 20,
|
||||
estVramGB: 8,
|
||||
},
|
||||
{
|
||||
id: "mistral-7b-q4",
|
||||
name: "mistral:7b-q4",
|
||||
sizeGB: 4.1,
|
||||
quantization: "Q4",
|
||||
device: "CPU",
|
||||
estCpuUsagePct: 75,
|
||||
estVramGB: 0,
|
||||
},
|
||||
{
|
||||
id: "mistral-7b-q8",
|
||||
name: "mistral:7b-q8",
|
||||
sizeGB: 7.9,
|
||||
quantization: "Q8",
|
||||
device: "GPU",
|
||||
estCpuUsagePct: 18,
|
||||
estVramGB: 8,
|
||||
},
|
||||
{
|
||||
id: "qwen2.5-7b-q4",
|
||||
name: "qwen2.5:7b-q4",
|
||||
sizeGB: 4.2,
|
||||
quantization: "Q4",
|
||||
device: "CPU",
|
||||
estCpuUsagePct: 80,
|
||||
estVramGB: 0,
|
||||
},
|
||||
{
|
||||
id: "qwen2.5-7b-q8",
|
||||
name: "qwen2.5:7b-q8",
|
||||
sizeGB: 8.1,
|
||||
quantization: "Q8",
|
||||
device: "GPU",
|
||||
estCpuUsagePct: 22,
|
||||
estVramGB: 8,
|
||||
},
|
||||
];
|
||||
|
||||
export function inferQuantization(nameOrDetails: string | unknown): string {
|
||||
const s =
|
||||
typeof nameOrDetails === "string" ? nameOrDetails.toLowerCase() : "";
|
||||
if (s.includes("q8")) return "Q8";
|
||||
if (s.includes("q5")) return "Q5";
|
||||
if (s.includes("q4")) return "Q4";
|
||||
return "Q?";
|
||||
}
|
||||
|
||||
export function inferDevice(details?: unknown): Device {
|
||||
const s = typeof details === "string" ? details.toLowerCase() : "";
|
||||
if (s.includes("cuda") || s.includes("metal") || s.includes("gpu"))
|
||||
return "GPU";
|
||||
return "CPU";
|
||||
}
|
||||
|
||||
export function estimateCpuVram(
|
||||
info: Pick<ModelInfo, "sizeGB" | "quantization" | "device">,
|
||||
) {
|
||||
const q = info.quantization.toUpperCase();
|
||||
let cpuPct = 50;
|
||||
let vramGB = 0;
|
||||
if (info.device === "GPU") {
|
||||
vramGB = Math.max(4, Math.round(info.sizeGB));
|
||||
cpuPct = 20;
|
||||
} else {
|
||||
// heurística simples: quantizações mais baixas usam mais CPU
|
||||
cpuPct = q === "Q8" ? 60 : q === "Q5" ? 70 : 85;
|
||||
}
|
||||
return { cpuPct, vramGB };
|
||||
}
|
||||
-200
@@ -1,200 +0,0 @@
|
||||
import { Ollama } from "ollama";
|
||||
import os from "node:os";
|
||||
import { spawn } from "node:child_process";
|
||||
import { getCatalogModels } from "./catalog";
|
||||
|
||||
export const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
|
||||
|
||||
export async function listModelsViaSdk() {
|
||||
try {
|
||||
const client = new Ollama({ host: OLLAMA_HOST });
|
||||
const res = await client.list();
|
||||
return res?.models ?? [];
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listModelsViaHttp() {
|
||||
const url = `${OLLAMA_HOST}/api/tags`;
|
||||
const res = await fetch(url, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`Ollama HTTP error ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data?.models ?? [];
|
||||
}
|
||||
|
||||
async function isOllamaHealthy(): Promise<boolean> {
|
||||
try {
|
||||
await listModelsViaHttp();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntilHealthy(timeoutMs: number, intervalMs = 800): Promise<boolean> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (await isOllamaHealthy()) return true;
|
||||
await new Promise((r) => setTimeout(r, intervalMs));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function ensureOllamaAvailable(options?: { timeoutMs?: number }): Promise<boolean> {
|
||||
const timeoutMs = options?.timeoutMs ?? 20000;
|
||||
if (await isOllamaHealthy()) return true;
|
||||
|
||||
// Windows: tentar iniciar via PowerShell script (WSL-first fallback Windows)
|
||||
if (os.platform() === "win32") {
|
||||
try {
|
||||
const child = spawn(
|
||||
"powershell",
|
||||
[
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"scripts/ensure-ollama.ps1",
|
||||
],
|
||||
{ stdio: "ignore", detached: true }
|
||||
);
|
||||
child.unref();
|
||||
} catch (e) {
|
||||
// Ignorar, tentaremos apenas aguardar
|
||||
}
|
||||
}
|
||||
|
||||
// Aguardar saúde
|
||||
return await waitUntilHealthy(timeoutMs);
|
||||
}
|
||||
|
||||
export async function chatWithStream(
|
||||
model: string,
|
||||
messages: Array<{ role: string; content: string }>,
|
||||
options?: Record<string, unknown>,
|
||||
system?: string,
|
||||
tools?: Array<any>
|
||||
) {
|
||||
const client = new Ollama({ host: OLLAMA_HOST });
|
||||
|
||||
const chatOptions: any = {
|
||||
model,
|
||||
messages: system
|
||||
? [{ role: "system", content: system }, ...messages]
|
||||
: messages,
|
||||
stream: true,
|
||||
options: { ...(options || {}), keep_alive: "5m" },
|
||||
};
|
||||
|
||||
// Adicionar tools se fornecidos
|
||||
if (tools && tools.length > 0) {
|
||||
chatOptions.tools = tools;
|
||||
}
|
||||
|
||||
return client.chat(chatOptions);
|
||||
}
|
||||
|
||||
export async function pullModel(model: string) {
|
||||
const client = new Ollama({ host: OLLAMA_HOST });
|
||||
return client.pull({ model, stream: true });
|
||||
}
|
||||
|
||||
export async function checkModelExists(modelName: string): Promise<boolean> {
|
||||
try {
|
||||
const models = await listModelsViaSdk();
|
||||
return models.some((model) => model.name === modelName);
|
||||
} catch (error) {
|
||||
console.error("Erro ao verificar modelos:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureModelExists(
|
||||
modelName: string,
|
||||
description?: string
|
||||
): Promise<void> {
|
||||
const exists = await checkModelExists(modelName);
|
||||
if (!exists) {
|
||||
console.log(`📥 Baixando modelo ${modelName}...`);
|
||||
if (description) {
|
||||
console.log(`ℹ️ ${description}`);
|
||||
}
|
||||
|
||||
const stream = await pullModel(modelName);
|
||||
let downloadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
let progress = 0;
|
||||
|
||||
// Processar stream para mostrar progresso
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.status === "downloading" && chunk.completed && chunk.total) {
|
||||
downloadedBytes = chunk.completed;
|
||||
totalBytes = chunk.total;
|
||||
progress = Math.round((downloadedBytes / totalBytes) * 100);
|
||||
|
||||
// Criar barra de progresso visual
|
||||
const barLength = 20;
|
||||
const filledLength = Math.round((progress / 100) * barLength);
|
||||
const bar =
|
||||
"█".repeat(filledLength) + "░".repeat(barLength - filledLength);
|
||||
|
||||
// Mostrar progresso com informações
|
||||
const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(1);
|
||||
const totalMB = (totalBytes / 1024 / 1024).toFixed(1);
|
||||
|
||||
process.stdout.write(
|
||||
`\r📥 ${modelName}: [${bar}] ${progress}% (${downloadedMB}MB/${totalMB}MB)`
|
||||
);
|
||||
} else if (chunk.status === "success") {
|
||||
console.log(`\n✅ Modelo ${modelName} baixado com sucesso!`);
|
||||
if (description) {
|
||||
console.log(
|
||||
`🎯 ${description.replace("será usado", "agora funciona")}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`✅ Modelo ${modelName} já está disponível`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteModel(modelName: string): Promise<void> {
|
||||
const client = new Ollama({ host: OLLAMA_HOST });
|
||||
await client.delete({ model: modelName });
|
||||
}
|
||||
|
||||
export async function searchRemoteModels(query?: string): Promise<any[]> {
|
||||
try {
|
||||
const catalog = await getCatalogModels();
|
||||
|
||||
// Normalizar pesquisa
|
||||
const q = (query || "").trim().toLowerCase();
|
||||
|
||||
const filtered = q
|
||||
? catalog.filter((m) => {
|
||||
const name = m.name?.toLowerCase() || "";
|
||||
const desc = m.description?.toLowerCase() || "";
|
||||
return name.includes(q) || desc.includes(q);
|
||||
})
|
||||
: catalog;
|
||||
|
||||
// Modelos instalados
|
||||
const installedModels = await listModelsViaSdk();
|
||||
const installedNames = new Set(installedModels.map((m) => m.name));
|
||||
|
||||
// Retorno no formato esperado pelo frontend
|
||||
return filtered.map((m) => ({
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
url: m.url,
|
||||
tags_count: m.tags_count,
|
||||
installed: installedNames.has(m.name),
|
||||
remote: true,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar modelos remotos:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
let db: Database.Database | null = null;
|
||||
|
||||
export async function getPreferencesDb(): Promise<Database.Database> {
|
||||
if (!db) {
|
||||
try {
|
||||
const dbPath = path.join(
|
||||
process.cwd(),
|
||||
"database",
|
||||
"ollahub-preferences.db"
|
||||
);
|
||||
|
||||
// Garantir que o diretório existe
|
||||
const dbDir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
db = new Database(dbPath);
|
||||
|
||||
// Configurações de performance
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("synchronous = NORMAL");
|
||||
db.pragma("foreign_keys = ON");
|
||||
db.pragma("cache_size = 10000");
|
||||
db.pragma("temp_store = memory");
|
||||
|
||||
// Criar tabela de preferências se não existir
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
preferences TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
console.log("✅ Banco de preferências SQLite conectado com sucesso");
|
||||
} catch (error) {
|
||||
console.warn("⚠️ Erro ao conectar banco de preferências:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
export async function getPreferences(userId: string = "default"): Promise<any> {
|
||||
try {
|
||||
const database = await getPreferencesDb();
|
||||
const stmt = database.prepare(
|
||||
"SELECT preferences FROM user_preferences WHERE user_id = ?"
|
||||
);
|
||||
const result = stmt.get(userId) as { preferences: string } | undefined;
|
||||
|
||||
if (result) {
|
||||
return JSON.parse(result.preferences);
|
||||
}
|
||||
|
||||
// Retornar preferências padrão
|
||||
return {
|
||||
selectedModel: null,
|
||||
systemPrompt: "",
|
||||
device: "auto",
|
||||
gpuIndex: 0,
|
||||
numGpu: 1,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar preferências:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setPreferences(
|
||||
userId: string = "default",
|
||||
preferences: any
|
||||
): Promise<void> {
|
||||
try {
|
||||
const database = await getPreferencesDb();
|
||||
const stmt = database.prepare(`
|
||||
INSERT OR REPLACE INTO user_preferences (user_id, preferences, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(userId, JSON.stringify(preferences), Date.now());
|
||||
console.log("✅ Preferências salvas no SQLite");
|
||||
} catch (error) {
|
||||
console.error("Erro ao salvar preferências:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function closePreferencesDb(): void {
|
||||
if (db) {
|
||||
db.close();
|
||||
db = null;
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
/**
|
||||
* Templates de prompt engineering para modelos sem suporte nativo a function calling
|
||||
*/
|
||||
|
||||
import type { MCPTool } from "@/lib/types/mcp-chat";
|
||||
|
||||
/**
|
||||
* Gera prompt de instruções para uso de tools via prompt engineering
|
||||
* @param tools Array de tools disponíveis
|
||||
* @returns String com instruções formatadas
|
||||
*/
|
||||
export function generateToolsPrompt(tools: MCPTool[]): string {
|
||||
if (tools.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const toolsDescription = tools
|
||||
.map((tool, index) => {
|
||||
const params = Object.entries(tool.inputSchema.properties || {})
|
||||
.map(([name, schema]: [string, any]) => {
|
||||
const required = tool.inputSchema.required?.includes(name)
|
||||
? " (required)"
|
||||
: " (optional)";
|
||||
const type = schema.type || "any";
|
||||
const desc = schema.description || "";
|
||||
return ` - ${name}: ${type}${required}${desc ? ` - ${desc}` : ""}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return `${index + 1}. **${tool.name}**
|
||||
Description: ${tool.description}
|
||||
Parameters:
|
||||
${params || " (no parameters)"}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
return `
|
||||
# FERRAMENTAS DISPONÍVEIS
|
||||
|
||||
Você tem acesso às seguintes ferramentas externas para auxiliar o usuário:
|
||||
|
||||
${toolsDescription}
|
||||
|
||||
## INSTRUÇÕES IMPORTANTES
|
||||
|
||||
1. **Quando usar ferramentas:**
|
||||
- Use ferramentas quando precisar de informações em tempo real
|
||||
- Use ferramentas quando precisar executar ações externas
|
||||
- Use ferramentas quando o usuário solicitar dados específicos
|
||||
|
||||
2. **Como usar ferramentas:**
|
||||
- Para chamar uma ferramenta, responda EXATAMENTE no seguinte formato JSON:
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"tool_call": {
|
||||
"name": "nome_da_ferramenta",
|
||||
"parameters": {
|
||||
"parametro1": "valor1",
|
||||
"parametro2": "valor2"
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
- NÃO adicione texto antes ou depois do JSON
|
||||
- Certifique-se de que o JSON está bem formatado
|
||||
- Use apenas ferramentas da lista acima
|
||||
|
||||
3. **REGRA CRUCIAL - NUNCA MENCIONE AO USUÁRIO:**
|
||||
- NUNCA mencione que você está usando ferramentas
|
||||
- NUNCA mencione MCPs ou APIs externas
|
||||
- NUNCA diga "vou usar a ferramenta X"
|
||||
- NUNCA explique o processo técnico de como obteve informações
|
||||
- Apresente as informações naturalmente como se fossem seu conhecimento
|
||||
|
||||
4. **Após receber resultado da ferramenta:**
|
||||
- Você receberá uma mensagem do sistema com o resultado
|
||||
- Integre o resultado naturalmente na sua resposta
|
||||
- Responda ao usuário de forma fluida e natural
|
||||
- Cite informações específicas do resultado quando relevante
|
||||
|
||||
## EXEMPLO
|
||||
|
||||
Usuário: "Qual é o clima em São Paulo?"
|
||||
|
||||
Sua resposta:
|
||||
\`\`\`json
|
||||
{
|
||||
"tool_call": {
|
||||
"name": "get_weather",
|
||||
"parameters": {
|
||||
"city": "São Paulo"
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
[Sistema retorna: {"temperature": 25, "condition": "sunny"}]
|
||||
|
||||
Sua resposta final ao usuário:
|
||||
"O clima em São Paulo está ensolarado com 25°C."
|
||||
|
||||
**NÃO faça:**
|
||||
"Vou consultar a ferramenta de clima para você..."
|
||||
"Usei o get_weather e obtive..."
|
||||
"De acordo com a API..."
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera prompt condensado para economia de tokens
|
||||
* @param tools Array de tools disponíveis
|
||||
* @returns String com instruções compactas
|
||||
*/
|
||||
export function generateCompactToolsPrompt(tools: MCPTool[]): string {
|
||||
if (tools.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const toolsList = tools
|
||||
.map((tool) => {
|
||||
const params = Object.keys(tool.inputSchema.properties || {}).join(", ");
|
||||
return `${tool.name}(${params}): ${tool.description}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return `
|
||||
TOOLS: ${tools.length} available
|
||||
${toolsList}
|
||||
|
||||
FORMAT: {"tool_call":{"name":"tool_name","parameters":{...}}}
|
||||
RULE: Never mention tools to user. Present results naturally.
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrai tool call de uma resposta do modelo
|
||||
* @param response Resposta do modelo
|
||||
* @returns Tool call extraído ou null
|
||||
*/
|
||||
export function extractToolCallFromResponse(response: string): {
|
||||
name: string;
|
||||
parameters: Record<string, any>;
|
||||
} | null {
|
||||
try {
|
||||
// Tentar extrair JSON da resposta
|
||||
const jsonMatch = response.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
const jsonText = jsonMatch ? jsonMatch[1] : response;
|
||||
|
||||
const parsed = JSON.parse(jsonText.trim());
|
||||
|
||||
// Formato atual suportado pelo prompt engineering
|
||||
if (parsed.tool_call && parsed.tool_call.name) {
|
||||
return {
|
||||
name: parsed.tool_call.name,
|
||||
parameters: parsed.tool_call.parameters || {},
|
||||
};
|
||||
}
|
||||
|
||||
// Formato OpenAI-like: { type: 'function', function: { name, parameters|arguments } }
|
||||
// Algumas implementações usam `arguments` como string JSON
|
||||
if (parsed.type === "function" && parsed.function && parsed.function.name) {
|
||||
const fn = parsed.function;
|
||||
let params: any = fn.parameters ?? fn.arguments ?? {};
|
||||
if (typeof params === "string") {
|
||||
try {
|
||||
params = JSON.parse(params);
|
||||
} catch {
|
||||
// se não for JSON válido, mantém string (executor fará fallback)
|
||||
}
|
||||
}
|
||||
return { name: fn.name, parameters: params || {} };
|
||||
}
|
||||
|
||||
// Formato simples: { name: "tool", parameters: {...} }
|
||||
if (parsed.name && (parsed.parameters || parsed.arguments)) {
|
||||
let params: any = parsed.parameters ?? parsed.arguments ?? {};
|
||||
if (typeof params === "string") {
|
||||
try {
|
||||
params = JSON.parse(params);
|
||||
} catch {
|
||||
// mantém string se não for JSON válido
|
||||
}
|
||||
}
|
||||
return { name: parsed.name, parameters: params || {} };
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
// Tentar extrair manualmente
|
||||
const nameMatch = response.match(/"name":\s*"([^"]+)"/);
|
||||
if (nameMatch) {
|
||||
const name = nameMatch[1];
|
||||
// Captura tanto parameters quanto arguments
|
||||
const paramsMatch =
|
||||
response.match(/"parameters"\s*:\s*({[\s\S]*?})/) ||
|
||||
response.match(/"arguments"\s*:\s*("[\s\S]*?"|{[\s\S]*?})/);
|
||||
let parameters: any = {};
|
||||
if (paramsMatch) {
|
||||
const raw = paramsMatch[1];
|
||||
try {
|
||||
parameters = JSON.parse(raw);
|
||||
} catch {
|
||||
// caso venha como string com JSON dentro de aspas
|
||||
if (raw.startsWith('"') && raw.endsWith('"')) {
|
||||
try {
|
||||
parameters = JSON.parse(raw.slice(1, -1));
|
||||
} catch {
|
||||
parameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { name, parameters };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata resultado de tool para injetar no contexto
|
||||
* @param toolName Nome do tool
|
||||
* @param result Resultado da execução
|
||||
* @returns Mensagem formatada para o sistema
|
||||
*/
|
||||
export function formatToolResultForContext(
|
||||
toolName: string,
|
||||
result: any
|
||||
): string {
|
||||
const resultJson =
|
||||
typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
||||
|
||||
return `[TOOL RESULT - ${toolName}]
|
||||
${resultJson}
|
||||
|
||||
[END TOOL RESULT]
|
||||
|
||||
Use as informações acima para responder ao usuário de forma natural. NÃO mencione que usou uma ferramenta.`;
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
import {
|
||||
DeepNLPSearchParams,
|
||||
DeepNLPResponse,
|
||||
MCPServerConfig,
|
||||
MCPTool,
|
||||
MCPProvider,
|
||||
} from "@/lib/types/mcp";
|
||||
import { MCPCache } from "@/lib/cache/mcp-cache";
|
||||
|
||||
const DEEPNLP_API_BASE = "https://www.deepnlp.org/api/mcp_marketplace/v1";
|
||||
|
||||
export class DeepNLPService {
|
||||
// Mapear categorias do projeto para categorias DeepNLP
|
||||
private static categoryMap = {
|
||||
productivity: ["office"],
|
||||
development: ["code", "browser"],
|
||||
data: ["database"],
|
||||
integration: ["search", "payment"],
|
||||
other: ["map", "chart", "finance"],
|
||||
};
|
||||
|
||||
// Mapear categorias DeepNLP para categorias internas
|
||||
private static reverseCategoryMap: Record<string, MCPProvider["category"]> = {
|
||||
office: "office",
|
||||
code: "code",
|
||||
browser: "browser",
|
||||
database: "database",
|
||||
search: "search",
|
||||
payment: "payment",
|
||||
map: "map",
|
||||
chart: "chart",
|
||||
finance: "finance",
|
||||
};
|
||||
|
||||
/**
|
||||
* Buscar servidores MCP na API DeepNLP
|
||||
*/
|
||||
static async searchMCPs(
|
||||
params: DeepNLPSearchParams,
|
||||
retryCount = 0
|
||||
): Promise<DeepNLPResponse> {
|
||||
const maxRetries = 3;
|
||||
const baseDelay = 1000; // 1 segundo
|
||||
|
||||
try {
|
||||
// Verificar cache primeiro
|
||||
const cacheKey = MCPCache.generateKey(params);
|
||||
const cachedResult = MCPCache.get(cacheKey);
|
||||
|
||||
if (cachedResult) {
|
||||
console.log("Returning cached search results");
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const url = new URL(DEEPNLP_API_BASE);
|
||||
|
||||
// Adicionar parâmetros de busca
|
||||
if (params.query) url.searchParams.set("query", params.query);
|
||||
if (params.category) url.searchParams.set("category", params.category);
|
||||
if (params.id) url.searchParams.set("id", params.id);
|
||||
if (params.page_id !== undefined)
|
||||
url.searchParams.set("page_id", params.page_id.toString());
|
||||
if (params.count_per_page !== undefined)
|
||||
url.searchParams.set(
|
||||
"count_per_page",
|
||||
params.count_per_page.toString()
|
||||
);
|
||||
if (params.offset !== undefined)
|
||||
url.searchParams.set("offset", params.offset.toString());
|
||||
if (params.mode) url.searchParams.set("mode", params.mode);
|
||||
|
||||
console.log(
|
||||
`DeepNLP API Request (attempt ${retryCount + 1}):`,
|
||||
url.toString()
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0",
|
||||
},
|
||||
// Timeout de 10 segundos
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`DeepNLP API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("DeepNLP API Response:", data);
|
||||
|
||||
// Salvar no cache
|
||||
MCPCache.setWithDataType(cacheKey, data, "search");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error calling DeepNLP API (attempt ${retryCount + 1}):`,
|
||||
error
|
||||
);
|
||||
|
||||
// Se ainda temos tentativas restantes, tentar novamente com backoff exponencial
|
||||
if (retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount); // Backoff exponencial
|
||||
console.log(`Retrying in ${delay}ms...`);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.searchMCPs(params, retryCount + 1);
|
||||
}
|
||||
|
||||
// Se esgotamos as tentativas, lançar erro
|
||||
throw new Error(
|
||||
`Failed to fetch MCPs after ${maxRetries + 1} attempts: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter configuração completa de um servidor específico
|
||||
*/
|
||||
static async getServerConfig(
|
||||
id: string,
|
||||
retryCount = 0
|
||||
): Promise<MCPServerConfig> {
|
||||
const maxRetries = 3;
|
||||
const baseDelay = 1000;
|
||||
|
||||
try {
|
||||
// Verificar cache primeiro
|
||||
const cacheKey = MCPCache.generateServerConfigKey(id);
|
||||
const cachedResult = MCPCache.get(cacheKey);
|
||||
|
||||
if (cachedResult) {
|
||||
console.log("Returning cached server config");
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const [owner, repo] = id.split("/");
|
||||
if (!owner || !repo) {
|
||||
throw new Error(
|
||||
`Invalid MCP ID format: ${id}. Expected format: owner/repo`
|
||||
);
|
||||
}
|
||||
|
||||
const url = `${DEEPNLP_API_BASE}/server/${owner}/${repo}`;
|
||||
console.log(
|
||||
`DeepNLP Server Config Request (attempt ${retryCount + 1}):`,
|
||||
url
|
||||
);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0",
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`DeepNLP Server Config API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("DeepNLP Server Config Response:", data);
|
||||
|
||||
// Salvar no cache
|
||||
MCPCache.setWithDataType(cacheKey, data, "config");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error getting server config from DeepNLP (attempt ${retryCount + 1}):`,
|
||||
error
|
||||
);
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
console.log(`Retrying server config in ${delay}ms...`);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.getServerConfig(id, retryCount + 1);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to get server config after ${maxRetries + 1} attempts: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listar ferramentas de um servidor específico
|
||||
*/
|
||||
static async getServerTools(id: string, retryCount = 0): Promise<MCPTool[]> {
|
||||
const maxRetries = 3;
|
||||
const baseDelay = 1000;
|
||||
|
||||
try {
|
||||
// Verificar cache primeiro
|
||||
const cacheKey = MCPCache.generateServerToolsKey(id);
|
||||
const cachedResult = MCPCache.get(cacheKey);
|
||||
|
||||
if (cachedResult) {
|
||||
console.log("Returning cached server tools");
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const [owner, repo] = id.split("/");
|
||||
if (!owner || !repo) {
|
||||
throw new Error(
|
||||
`Invalid MCP ID format: ${id}. Expected format: owner/repo`
|
||||
);
|
||||
}
|
||||
|
||||
const url = `${DEEPNLP_API_BASE}/tools/${owner}/${repo}`;
|
||||
console.log(
|
||||
`DeepNLP Server Tools Request (attempt ${retryCount + 1}):`,
|
||||
url
|
||||
);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0",
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`DeepNLP Server Tools API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("DeepNLP Server Tools Response:", data);
|
||||
|
||||
const tools = data.tools || [];
|
||||
|
||||
// Salvar no cache
|
||||
MCPCache.setWithDataType(cacheKey, tools, "tools");
|
||||
|
||||
return tools;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error getting server tools from DeepNLP (attempt ${retryCount + 1}):`,
|
||||
error
|
||||
);
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
console.log(`Retrying server tools in ${delay}ms...`);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.getServerTools(id, retryCount + 1);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to get server tools after ${maxRetries + 1} attempts: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformar resposta DeepNLP para formato interno MCPProvider
|
||||
*/
|
||||
static transformToMCPProvider(deepnlpItem: any): MCPProvider {
|
||||
const category = this.mapCategory(deepnlpItem.subfield || "other");
|
||||
|
||||
return {
|
||||
id: deepnlpItem.id || "",
|
||||
name: deepnlpItem.content_name || "",
|
||||
author: deepnlpItem.publisher_id || "",
|
||||
description: deepnlpItem.description || "",
|
||||
version: "1.0.0", // DeepNLP não fornece versão específica
|
||||
category,
|
||||
tags: deepnlpItem.content_tag_list
|
||||
? deepnlpItem.content_tag_list
|
||||
.split(",")
|
||||
.map((tag: string) => tag.trim())
|
||||
: [],
|
||||
rating: parseFloat(deepnlpItem.rating || "0"),
|
||||
totalRatings: parseInt(deepnlpItem.review_cnt || "0"),
|
||||
repository: deepnlpItem.website,
|
||||
homepage: deepnlpItem.website,
|
||||
installed: false, // Será verificado localmente
|
||||
subfield: deepnlpItem.subfield,
|
||||
field: deepnlpItem.field,
|
||||
config: deepnlpItem.config || [],
|
||||
tools: deepnlpItem.ext_info?.tools || [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapear categoria DeepNLP para categoria interna
|
||||
*/
|
||||
static mapCategory(subfield: string): MCPProvider["category"] {
|
||||
return this.reverseCategoryMap[subfield?.toLowerCase()] || "other";
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter mapeamento de categoria interna para DeepNLP
|
||||
*/
|
||||
static getCategoryMapping(internalCategory: string): string | undefined {
|
||||
const mappings =
|
||||
this.categoryMap[internalCategory as keyof typeof this.categoryMap];
|
||||
return mappings?.[0]; // Retorna a primeira categoria DeepNLP correspondente
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se a API está disponível
|
||||
*/
|
||||
static async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${DEEPNLP_API_BASE}?count_per_page=1`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0",
|
||||
},
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error("DeepNLP API health check failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
/**
|
||||
* Serviço para detectar intenções do usuário através de palavras-chave
|
||||
*/
|
||||
|
||||
import type { DetectedIntent } from "@/lib/types/mcp-chat";
|
||||
|
||||
/**
|
||||
* Padrões de keywords para cada tipo de intent
|
||||
*/
|
||||
const INTENT_PATTERNS = {
|
||||
web_search: {
|
||||
keywords: [
|
||||
"buscar",
|
||||
"pesquisar",
|
||||
"procurar",
|
||||
"google",
|
||||
"encontrar",
|
||||
"search",
|
||||
"look up",
|
||||
"find",
|
||||
"o que é",
|
||||
"quem é",
|
||||
"onde fica",
|
||||
],
|
||||
regex:
|
||||
/\b(buscar|pesquisar|procurar|google|encontrar|search|look up|find)\b/i,
|
||||
},
|
||||
time: {
|
||||
keywords: [
|
||||
"que horas",
|
||||
"data",
|
||||
"horário",
|
||||
"hoje",
|
||||
"agora",
|
||||
"quando",
|
||||
"calendário",
|
||||
"time",
|
||||
"date",
|
||||
"current time",
|
||||
"what time",
|
||||
],
|
||||
regex:
|
||||
/\b(que horas|data|horário|hoje|agora|quando|time|date|current time)\b/i,
|
||||
},
|
||||
weather: {
|
||||
keywords: [
|
||||
"clima",
|
||||
"tempo",
|
||||
"temperatura",
|
||||
"chuva",
|
||||
"previsão",
|
||||
"weather",
|
||||
"forecast",
|
||||
"temperature",
|
||||
],
|
||||
regex:
|
||||
/\b(clima|tempo|temperatura|chuva|previsão|weather|forecast|temperature)\b/i,
|
||||
},
|
||||
file: {
|
||||
keywords: [
|
||||
"arquivo",
|
||||
"pasta",
|
||||
"diretório",
|
||||
"file",
|
||||
"folder",
|
||||
"directory",
|
||||
"ler arquivo",
|
||||
"read file",
|
||||
"listar",
|
||||
"list",
|
||||
],
|
||||
regex:
|
||||
/\b(arquivo|pasta|diretório|file|folder|directory|ler arquivo|read file)\b/i,
|
||||
},
|
||||
code: {
|
||||
keywords: [
|
||||
"código",
|
||||
"programar",
|
||||
"executar",
|
||||
"run",
|
||||
"execute",
|
||||
"compile",
|
||||
"debug",
|
||||
"git",
|
||||
"commit",
|
||||
],
|
||||
regex:
|
||||
/\b(código|programar|executar|run|execute|compile|debug|git|commit)\b/i,
|
||||
},
|
||||
database: {
|
||||
keywords: [
|
||||
"banco de dados",
|
||||
"database",
|
||||
"query",
|
||||
"sql",
|
||||
"select",
|
||||
"insert",
|
||||
"update",
|
||||
"delete",
|
||||
],
|
||||
regex:
|
||||
/\b(banco de dados|database|query|sql|select|insert|update|delete)\b/i,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Detecta a intenção do usuário na mensagem
|
||||
* @param message Mensagem do usuário
|
||||
* @returns Intent detectado ou null
|
||||
*/
|
||||
export function detectIntent(message: string): DetectedIntent | null {
|
||||
const messageLower = message.toLowerCase();
|
||||
|
||||
for (const [type, pattern] of Object.entries(INTENT_PATTERNS)) {
|
||||
const matches = pattern.regex.test(messageLower);
|
||||
|
||||
if (matches) {
|
||||
// Calcular confiança baseado em quantas keywords foram encontradas
|
||||
const foundKeywords = pattern.keywords.filter((keyword) =>
|
||||
messageLower.includes(keyword.toLowerCase())
|
||||
);
|
||||
|
||||
const confidence = Math.min(0.5 + foundKeywords.length * 0.2, 1.0);
|
||||
|
||||
return {
|
||||
type: type as DetectedIntent["type"],
|
||||
confidence,
|
||||
keywords: foundKeywords,
|
||||
suggestedTools: getSuggestedToolsForIntent(
|
||||
type as DetectedIntent["type"]
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna tools sugeridos para um tipo de intent
|
||||
* @param intentType Tipo de intent
|
||||
* @returns Array de nomes de tools sugeridos
|
||||
*/
|
||||
function getSuggestedToolsForIntent(
|
||||
intentType: DetectedIntent["type"]
|
||||
): string[] {
|
||||
const toolMapping: Record<DetectedIntent["type"], string[]> = {
|
||||
web_search: ["web_search", "duckduckgo_search", "google_search", "search"],
|
||||
time: ["get_current_time", "get_date", "current_time", "time"],
|
||||
weather: ["get_weather", "weather", "forecast"],
|
||||
file: ["read_file", "list_files", "filesystem", "file"],
|
||||
code: ["execute_code", "git", "run_command"],
|
||||
database: ["query", "sql", "database"],
|
||||
other: [],
|
||||
};
|
||||
|
||||
return toolMapping[intentType] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se um tool name corresponde a algum intent detectado
|
||||
* @param toolName Nome do tool
|
||||
* @param intent Intent detectado
|
||||
* @returns true se o tool é relevante para o intent
|
||||
*/
|
||||
export function isToolRelevantForIntent(
|
||||
toolName: string,
|
||||
intent: DetectedIntent
|
||||
): boolean {
|
||||
const toolNameLower = toolName.toLowerCase();
|
||||
return intent.suggestedTools.some((suggested) =>
|
||||
toolNameLower.includes(suggested.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtra tools com base em um intent detectado
|
||||
* @param tools Array de tools disponíveis
|
||||
* @param intent Intent detectado
|
||||
* @returns Tools filtrados e ordenados por relevância
|
||||
*/
|
||||
export function filterToolsByIntent(
|
||||
tools: Array<{ name: string; description: string }>,
|
||||
intent: DetectedIntent
|
||||
): Array<{ name: string; description: string; relevance: number }> {
|
||||
return tools
|
||||
.map((tool) => {
|
||||
let relevance = 0;
|
||||
|
||||
// Verificar se o tool name contém alguma keyword sugerida
|
||||
const toolNameLower = tool.name.toLowerCase();
|
||||
for (const suggested of intent.suggestedTools) {
|
||||
if (toolNameLower.includes(suggested.toLowerCase())) {
|
||||
relevance += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se a descrição contém keywords do intent
|
||||
const descriptionLower = tool.description.toLowerCase();
|
||||
for (const keyword of intent.keywords) {
|
||||
if (descriptionLower.includes(keyword.toLowerCase())) {
|
||||
relevance += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...tool, relevance };
|
||||
})
|
||||
.filter((tool) => tool.relevance > 0)
|
||||
.sort((a, b) => b.relevance - a.relevance);
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
/**
|
||||
* Serviço de gerenciamento de ambientes isolados para servidores MCP
|
||||
*/
|
||||
|
||||
import { mkdir, rm, access } from "fs/promises";
|
||||
import path from "path";
|
||||
import { MCP_CONFIG } from "@/lib/config/mcp";
|
||||
import { commandExists, spawnWithProgress } from "@/lib/utils/system";
|
||||
import type {
|
||||
MCPEnvironment,
|
||||
SystemDependencies,
|
||||
} from "@/lib/types/mcp-installer";
|
||||
|
||||
export class MCPEnvironmentService {
|
||||
/**
|
||||
* Verifica dependências do sistema
|
||||
*/
|
||||
static async checkSystemDependencies(): Promise<SystemDependencies> {
|
||||
const [node, npm, python, pip, cargo, git] = await Promise.all([
|
||||
commandExists("node"),
|
||||
commandExists("npm"),
|
||||
commandExists("python3"),
|
||||
commandExists("pip3"),
|
||||
commandExists("cargo"),
|
||||
commandExists("git"),
|
||||
]);
|
||||
|
||||
return { node, npm, python, pip, cargo, git };
|
||||
}
|
||||
|
||||
/**
|
||||
* Garante que o diretório de ambiente existe
|
||||
*/
|
||||
static async ensureEnvironmentDirectory(
|
||||
type: "npm" | "python" | "rust"
|
||||
): Promise<string> {
|
||||
let dir: string;
|
||||
|
||||
switch (type) {
|
||||
case "npm":
|
||||
dir = MCP_CONFIG.npmDir;
|
||||
break;
|
||||
case "python":
|
||||
dir = MCP_CONFIG.pythonDir;
|
||||
break;
|
||||
case "rust":
|
||||
dir = MCP_CONFIG.rustDir;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
await mkdir(dir, { recursive: true });
|
||||
console.log(`✓ Environment directory ensured: ${dir}`);
|
||||
return dir;
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Failed to create environment directory: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria ambiente virtual Python para um MCP específico
|
||||
*/
|
||||
static async createPythonVenv(
|
||||
mcpId: string,
|
||||
onProgress?: (message: string) => void
|
||||
): Promise<string> {
|
||||
const venvPath = path.join(MCP_CONFIG.pythonDir, mcpId);
|
||||
|
||||
// Verificar se já existe
|
||||
try {
|
||||
await access(venvPath);
|
||||
onProgress?.(`Virtual environment already exists at ${venvPath}`);
|
||||
return venvPath;
|
||||
} catch {
|
||||
// Não existe, criar novo
|
||||
}
|
||||
|
||||
onProgress?.("Creating Python virtual environment...");
|
||||
|
||||
try {
|
||||
const result = await spawnWithProgress(
|
||||
"python3",
|
||||
["-m", "venv", venvPath],
|
||||
{
|
||||
onOutput: (data) => onProgress?.(data.trim()),
|
||||
timeout: 60000, // 1 minuto para criar venv
|
||||
}
|
||||
);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to create venv: ${result.stderr || result.stdout}`
|
||||
);
|
||||
}
|
||||
|
||||
onProgress?.(`✓ Virtual environment created at ${venvPath}`);
|
||||
return venvPath;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to create Python venv: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém caminho do executável para um ambiente
|
||||
*/
|
||||
static getExecutablePath(
|
||||
type: "npm" | "python" | "rust",
|
||||
mcpId: string,
|
||||
packageName?: string
|
||||
): { executable: string; args: string[] } {
|
||||
switch (type) {
|
||||
case "npm":
|
||||
// Para npm, usar npx para executar pacotes instalados localmente
|
||||
return {
|
||||
executable: "npx",
|
||||
args: packageName ? ["--prefix", MCP_CONFIG.npmDir, packageName] : [],
|
||||
};
|
||||
|
||||
case "python": {
|
||||
const venvPath = path.join(MCP_CONFIG.pythonDir, mcpId);
|
||||
const pythonBin =
|
||||
process.platform === "win32"
|
||||
? path.join(venvPath, "Scripts", "python.exe")
|
||||
: path.join(venvPath, "bin", "python");
|
||||
|
||||
return {
|
||||
executable: pythonBin,
|
||||
args: packageName ? ["-m", packageName] : [],
|
||||
};
|
||||
}
|
||||
|
||||
case "rust": {
|
||||
// Para Rust, o binário estará em target/release/
|
||||
const binaryPath = path.join(
|
||||
MCP_CONFIG.rustDir,
|
||||
mcpId,
|
||||
"target",
|
||||
"release",
|
||||
packageName || mcpId
|
||||
);
|
||||
|
||||
return {
|
||||
executable: binaryPath,
|
||||
args: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa ambiente de um MCP (desinstalação)
|
||||
*/
|
||||
static async cleanupEnvironment(environment: MCPEnvironment): Promise<void> {
|
||||
try {
|
||||
console.log(`Cleaning up environment at ${environment.path}`);
|
||||
|
||||
await rm(environment.path, { recursive: true, force: true });
|
||||
|
||||
console.log(`✓ Environment cleaned: ${environment.path}`);
|
||||
} catch (error: any) {
|
||||
console.error(`Warning: Failed to cleanup environment: ${error.message}`);
|
||||
// Não lançar erro - cleanup é best-effort
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se um ambiente está válido
|
||||
*/
|
||||
static async validateEnvironment(
|
||||
environment: MCPEnvironment
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Verificar se o diretório existe
|
||||
await access(environment.path);
|
||||
|
||||
// Verificar se o executável existe
|
||||
await access(environment.executable);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações sobre o ambiente de um MCP
|
||||
*/
|
||||
static async getEnvironmentInfo(
|
||||
mcpId: string,
|
||||
type: "npm" | "python" | "rust"
|
||||
): Promise<{
|
||||
exists: boolean;
|
||||
path: string;
|
||||
size?: number;
|
||||
}> {
|
||||
const envPath = path.join(
|
||||
type === "npm"
|
||||
? MCP_CONFIG.npmDir
|
||||
: type === "python"
|
||||
? path.join(MCP_CONFIG.pythonDir, mcpId)
|
||||
: path.join(MCP_CONFIG.rustDir, mcpId)
|
||||
);
|
||||
|
||||
try {
|
||||
await access(envPath);
|
||||
// TODO: Calcular tamanho do diretório se necessário
|
||||
return { exists: true, path: envPath };
|
||||
} catch {
|
||||
return { exists: false, path: envPath };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
/**
|
||||
* Serviço executor de MCPs - spawna processos e executa tools via JSON-RPC
|
||||
*/
|
||||
|
||||
import { spawn, type ChildProcess } from "child_process";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import type { MCPExecutionResult, MCPTool } from "@/lib/types/mcp-chat";
|
||||
|
||||
const EXECUTION_TIMEOUT = 30000; // 30 segundos
|
||||
|
||||
export class MCPExecutor {
|
||||
/**
|
||||
* Executa um tool de MCP
|
||||
* @param mcpId ID do MCP instalado
|
||||
* @param toolName Nome do tool a executar
|
||||
* @param parameters Parâmetros do tool
|
||||
* @returns Resultado da execução
|
||||
*/
|
||||
static async executeMCPTool(
|
||||
mcpId: string,
|
||||
toolName: string,
|
||||
parameters: Record<string, any>
|
||||
): Promise<MCPExecutionResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Obter dados do MCP instalado
|
||||
const mcpData = MCPRepository.getInstalledMCP(mcpId);
|
||||
if (!mcpData) {
|
||||
throw new Error(`MCP ${mcpId} not found or not installed`);
|
||||
}
|
||||
|
||||
// Verificar se está pronto
|
||||
// Se status é undefined/null, considerar como "ready" (MCPs instalados antes da implementação de status)
|
||||
if (mcpData.status && mcpData.status !== "ready") {
|
||||
throw new Error(
|
||||
`MCP ${mcpId} is not ready (status: ${mcpData.status})`
|
||||
);
|
||||
}
|
||||
|
||||
// Obter comando executável
|
||||
// mcpData.config já vem parseado do repositório, mas pode ser string em casos legados
|
||||
const config =
|
||||
typeof mcpData.config === "string"
|
||||
? JSON.parse(mcpData.config)
|
||||
: mcpData.config || {};
|
||||
const commandFromConfig = config.command;
|
||||
const argsFromConfig = Array.isArray(config.args) ? config.args : [];
|
||||
|
||||
let command = commandFromConfig;
|
||||
let args = argsFromConfig;
|
||||
|
||||
// Se não houver command em config, reconstruir a partir de executable_command (se existir)
|
||||
if (!command && mcpData.executable_command) {
|
||||
const parts = String(mcpData.executable_command).trim().split(/\s+/);
|
||||
command = parts[0];
|
||||
args = parts.slice(1);
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
throw new Error(`No executable command found for MCP ${mcpId}`);
|
||||
}
|
||||
|
||||
console.log(`🚀 Executing MCP tool: ${mcpId}.${toolName}`);
|
||||
console.log(`Command: ${command} ${args.join(" ")}`);
|
||||
|
||||
// Spawnar processo do MCP
|
||||
const result = await this.spawnMCPProcess(
|
||||
command,
|
||||
args,
|
||||
toolName,
|
||||
parameters,
|
||||
mcpData.environment_path
|
||||
);
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
toolName,
|
||||
mcpId,
|
||||
result,
|
||||
executionTime,
|
||||
};
|
||||
} catch (error: any) {
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
console.error(`❌ Error executing MCP tool ${mcpId}.${toolName}:`, error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
toolName,
|
||||
mcpId,
|
||||
error: error.message || "Unknown error",
|
||||
executionTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawna processo do MCP e executa JSON-RPC
|
||||
* @param command Comando executável
|
||||
* @param args Argumentos do comando
|
||||
* @param toolName Nome do tool
|
||||
* @param parameters Parâmetros do tool
|
||||
* @param environmentPath Caminho do ambiente (opcional)
|
||||
* @returns Resultado do tool
|
||||
*/
|
||||
private static async spawnMCPProcess(
|
||||
command: string,
|
||||
args: string[],
|
||||
toolName: string,
|
||||
parameters: Record<string, any>,
|
||||
environmentPath?: string
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const env = {
|
||||
...globalThis.process.env,
|
||||
...(environmentPath && {
|
||||
PATH: `${environmentPath}/node_modules/.bin:${globalThis.process.env.PATH}`,
|
||||
}),
|
||||
};
|
||||
|
||||
const childProcess = spawn(command, args, {
|
||||
cwd: environmentPath || globalThis.process.cwd(),
|
||||
env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let responseReceived = false;
|
||||
|
||||
// Timeout
|
||||
const timeout = setTimeout(() => {
|
||||
if (!responseReceived) {
|
||||
childProcess.kill();
|
||||
reject(new Error(`Execution timeout after ${EXECUTION_TIMEOUT}ms`));
|
||||
}
|
||||
}, EXECUTION_TIMEOUT);
|
||||
|
||||
// Capturar stdout
|
||||
childProcess.stdout?.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
// Loga por linhas para diagnóstico
|
||||
const printedLines = data
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter((l: string) => l.trim().length > 0);
|
||||
for (const l of printedLines) {
|
||||
console.log(`📥 [MCP stdout] ${l}`);
|
||||
}
|
||||
|
||||
// Tentar parsear respostas JSON-RPC
|
||||
const parsedLines = stdout.split("\n");
|
||||
for (const line of parsedLines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
console.log("🔎 [MCP stdout parsed]", parsed);
|
||||
if (parsed.id === 2 && parsed.result) {
|
||||
// Resposta do tools/call
|
||||
clearTimeout(timeout);
|
||||
responseReceived = true;
|
||||
childProcess.kill();
|
||||
resolve(parsed.result);
|
||||
}
|
||||
} catch (e) {
|
||||
// Linha não é JSON válido, continuar
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Capturar stderr
|
||||
childProcess.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
const errLines = data
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter((l: string) => l.trim().length > 0);
|
||||
for (const l of errLines) {
|
||||
console.warn(`⚠️ [MCP stderr] ${l}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Erro de processo
|
||||
childProcess.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`Process error: ${error.message}`));
|
||||
});
|
||||
|
||||
// Processo encerrou
|
||||
childProcess.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
if (!responseReceived) {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(`Process exited with code ${code}. stderr: ${stderr}`)
|
||||
);
|
||||
} else {
|
||||
reject(new Error("No valid response received from MCP"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Enviar requisições JSON-RPC
|
||||
try {
|
||||
// 1. Initialize
|
||||
const initRequest = {
|
||||
jsonrpc: "2.0",
|
||||
method: "initialize",
|
||||
params: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "ollahub", version: "1.0.0" },
|
||||
},
|
||||
id: 1,
|
||||
};
|
||||
console.log("📤 [MCP send] initialize", initRequest);
|
||||
childProcess.stdin?.write(JSON.stringify(initRequest) + "\n");
|
||||
|
||||
// Aguardar um pouco antes de enviar tool call
|
||||
setTimeout(() => {
|
||||
// 2. Call tool
|
||||
const toolCallRequest = {
|
||||
jsonrpc: "2.0",
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: parameters,
|
||||
},
|
||||
id: 2,
|
||||
};
|
||||
console.log("📤 [MCP send] tools/call", toolCallRequest);
|
||||
childProcess.stdin?.write(JSON.stringify(toolCallRequest) + "\n");
|
||||
childProcess.stdin?.end();
|
||||
}, 500);
|
||||
} catch (error: any) {
|
||||
clearTimeout(timeout);
|
||||
childProcess.kill();
|
||||
reject(new Error(`Failed to send JSON-RPC request: ${error.message}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter tools disponíveis de um MCP
|
||||
* @param mcpId ID do MCP
|
||||
* @returns Array de tools
|
||||
*/
|
||||
static getAvailableTools(mcpId: string): MCPTool[] {
|
||||
try {
|
||||
const mcpData = MCPRepository.getInstalledMCP(mcpId);
|
||||
if (!mcpData || !mcpData.tools) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Se já é um array (já foi parseado), retornar diretamente
|
||||
if (Array.isArray(mcpData.tools)) {
|
||||
return mcpData.tools;
|
||||
}
|
||||
|
||||
// Se é string, parsear
|
||||
if (typeof mcpData.tools === "string") {
|
||||
const tools = JSON.parse(mcpData.tools);
|
||||
return Array.isArray(tools) ? tools : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error(`Error getting available tools for ${mcpId}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar parâmetros contra schema do tool
|
||||
* @param parameters Parâmetros fornecidos
|
||||
* @param tool Definição do tool
|
||||
* @returns true se válido, false caso contrário
|
||||
*/
|
||||
static validateParameters(
|
||||
parameters: Record<string, any>,
|
||||
tool: MCPTool
|
||||
): { valid: boolean; error?: string } {
|
||||
try {
|
||||
const schema = tool.inputSchema;
|
||||
|
||||
// Verificar campos obrigatórios
|
||||
if (schema.required) {
|
||||
for (const required of schema.required) {
|
||||
if (!(required in parameters)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Missing required parameter: ${required}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validação básica de tipos
|
||||
for (const [key, value] of Object.entries(parameters)) {
|
||||
if (schema.properties && schema.properties[key]) {
|
||||
const propSchema = schema.properties[key];
|
||||
const expectedType = propSchema.type;
|
||||
|
||||
if (expectedType) {
|
||||
const actualType = typeof value;
|
||||
// Tratar objeto vazio como "ausente" quando tipo esperado não é object
|
||||
if (
|
||||
value &&
|
||||
actualType === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
Object.keys(value).length === 0 &&
|
||||
expectedType !== "object"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const valid =
|
||||
(expectedType === "string" && actualType === "string") ||
|
||||
(expectedType === "number" && actualType === "number") ||
|
||||
(expectedType === "boolean" && actualType === "boolean") ||
|
||||
(expectedType === "object" && actualType === "object") ||
|
||||
(expectedType === "array" && Array.isArray(value));
|
||||
|
||||
if (!valid) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${key} should be ${expectedType}, got ${actualType}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch (error: any) {
|
||||
return { valid: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
/**
|
||||
* Serviço de instalação de servidores MCP
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import { MCP_CONFIG } from "@/lib/config/mcp";
|
||||
import { MCPEnvironmentService } from "./mcp-environment";
|
||||
import { sanitizePackageName, spawnWithProgress } from "@/lib/utils/system";
|
||||
import type {
|
||||
MCPEnvironment,
|
||||
InstallationProgress,
|
||||
MCPInstallationStatus,
|
||||
InstallationConfig,
|
||||
} from "@/lib/types/mcp-installer";
|
||||
|
||||
export class MCPInstallerService {
|
||||
/**
|
||||
* Instala um servidor MCP baseado na configuração
|
||||
*/
|
||||
static async install(
|
||||
config: InstallationConfig,
|
||||
onProgress: (progress: InstallationProgress) => void
|
||||
): Promise<MCPEnvironment> {
|
||||
// Verificar dependências do sistema
|
||||
onProgress({
|
||||
status: "checking_dependencies" as MCPInstallationStatus,
|
||||
message: "Checking system dependencies...",
|
||||
percentage: 5,
|
||||
});
|
||||
|
||||
const dependencies = await MCPEnvironmentService.checkSystemDependencies();
|
||||
|
||||
// Determinar tipo de instalação
|
||||
const installType = this.determineInstallType(config, dependencies);
|
||||
|
||||
onProgress({
|
||||
status: "downloading" as MCPInstallationStatus,
|
||||
message: `Preparing ${installType} installation...`,
|
||||
percentage: 10,
|
||||
});
|
||||
|
||||
// Delegar para instalador específico
|
||||
switch (installType) {
|
||||
case "npm":
|
||||
return await this.installNPM(config, onProgress, dependencies);
|
||||
case "python":
|
||||
return await this.installPython(config, onProgress, dependencies);
|
||||
case "rust":
|
||||
return await this.installRust(config, onProgress, dependencies);
|
||||
default:
|
||||
throw new Error(`Unsupported installation type: ${installType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determina o tipo de instalação baseado na configuração
|
||||
*/
|
||||
private static determineInstallType(
|
||||
config: InstallationConfig,
|
||||
dependencies: any
|
||||
): "npm" | "python" | "rust" {
|
||||
// Preferir package_registry se disponível
|
||||
if (config.packageRegistry === "npm" && dependencies.npm) {
|
||||
return "npm";
|
||||
}
|
||||
if (config.packageRegistry === "pypi" && dependencies.python) {
|
||||
return "python";
|
||||
}
|
||||
if (config.packageRegistry === "cargo" && dependencies.cargo) {
|
||||
return "rust";
|
||||
}
|
||||
|
||||
// Tentar detectar pelo nome do pacote
|
||||
if (config.packageName) {
|
||||
if (
|
||||
config.packageName.startsWith("@") ||
|
||||
config.packageName.includes("/")
|
||||
) {
|
||||
if (dependencies.npm) return "npm";
|
||||
}
|
||||
if (
|
||||
config.packageName.includes("mcp-server") ||
|
||||
config.packageName.includes("mcp_server")
|
||||
) {
|
||||
if (dependencies.python) return "python";
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback para npm se disponível
|
||||
if (dependencies.npm) return "npm";
|
||||
if (dependencies.python) return "python";
|
||||
if (dependencies.cargo) return "rust";
|
||||
|
||||
throw new Error("No suitable package manager found");
|
||||
}
|
||||
|
||||
/**
|
||||
* Instala pacote NPM
|
||||
*/
|
||||
private static async installNPM(
|
||||
config: InstallationConfig,
|
||||
onProgress: (progress: InstallationProgress) => void,
|
||||
dependencies: any
|
||||
): Promise<MCPEnvironment> {
|
||||
if (!dependencies.npm) {
|
||||
throw new Error("npm is not installed on this system");
|
||||
}
|
||||
|
||||
// Sanitizar nome do pacote
|
||||
const packageName = sanitizePackageName(config.packageName);
|
||||
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: `Installing npm package: ${packageName}`,
|
||||
percentage: 20,
|
||||
logs: config.enableLogs
|
||||
? [`Installing ${packageName} via npm...`]
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Garantir diretório npm existe
|
||||
const npmDir = await MCPEnvironmentService.ensureEnvironmentDirectory(
|
||||
"npm"
|
||||
);
|
||||
|
||||
// Instalar pacote localmente no diretório npm
|
||||
const logs: string[] = [];
|
||||
|
||||
try {
|
||||
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const result = await spawnWithProgress(
|
||||
npmBin,
|
||||
["install", packageName, "--prefix", npmDir],
|
||||
{
|
||||
cwd: npmDir,
|
||||
timeout: MCP_CONFIG.installTimeout,
|
||||
onOutput: (data, isError) => {
|
||||
if (config.enableLogs) {
|
||||
logs.push(data);
|
||||
}
|
||||
|
||||
// Atualizar progresso baseado na saída
|
||||
let percentage = 40;
|
||||
if (data.includes("fetchMetadata")) percentage = 30;
|
||||
if (data.includes("extract")) percentage = 50;
|
||||
if (data.includes("finalize")) percentage = 70;
|
||||
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: isError
|
||||
? `npm: ${data.substring(0, 100)}`
|
||||
: `Installing: ${data.substring(0, 100)}`,
|
||||
percentage,
|
||||
logs: config.enableLogs ? [data] : undefined,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`npm install failed: ${result.stderr || result.stdout}`
|
||||
);
|
||||
}
|
||||
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: "npm package installed successfully",
|
||||
percentage: 80,
|
||||
logs: config.enableLogs ? ["✓ Installation completed"] : undefined,
|
||||
});
|
||||
|
||||
// Construir ambiente
|
||||
// Para NPM, executar npx do diretório onde foi instalado (não usa --prefix que não funciona)
|
||||
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
|
||||
const environment: MCPEnvironment = {
|
||||
type: "npm",
|
||||
path: npmDir,
|
||||
executable: npxBin,
|
||||
args: ["-y", packageName],
|
||||
};
|
||||
|
||||
return environment;
|
||||
} catch (error: any) {
|
||||
const msg = String(error?.message || "");
|
||||
if (msg.includes("spawn EINVAL") || msg.includes("Failed to spawn process") || msg.includes("ENOENT")) {
|
||||
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: "Falling back to npx runtime execution (no preinstall)",
|
||||
percentage: 60,
|
||||
logs: config.enableLogs ? [msg] : undefined,
|
||||
});
|
||||
|
||||
const environment: MCPEnvironment = {
|
||||
type: "npm",
|
||||
path: npmDir,
|
||||
executable: npxBin,
|
||||
args: ["-y", packageName],
|
||||
};
|
||||
return environment;
|
||||
}
|
||||
throw new Error(`NPM installation failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instala pacote Python
|
||||
*/
|
||||
private static async installPython(
|
||||
config: InstallationConfig,
|
||||
onProgress: (progress: InstallationProgress) => void,
|
||||
dependencies: any
|
||||
): Promise<MCPEnvironment> {
|
||||
if (!dependencies.python || !dependencies.pip) {
|
||||
throw new Error("Python3 and pip3 are not installed on this system");
|
||||
}
|
||||
|
||||
const packageName = sanitizePackageName(config.packageName);
|
||||
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: "Creating Python virtual environment...",
|
||||
percentage: 20,
|
||||
logs: config.enableLogs ? ["Creating venv..."] : undefined,
|
||||
});
|
||||
|
||||
// Garantir diretório python existe
|
||||
await MCPEnvironmentService.ensureEnvironmentDirectory("python");
|
||||
|
||||
// Criar venv para este MCP
|
||||
const venvPath = await MCPEnvironmentService.createPythonVenv(
|
||||
config.mcpId,
|
||||
(msg) => {
|
||||
if (config.enableLogs) {
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: msg,
|
||||
percentage: 30,
|
||||
logs: [msg],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: `Installing Python package: ${packageName}`,
|
||||
percentage: 40,
|
||||
logs: config.enableLogs
|
||||
? [`Installing ${packageName} via pip...`]
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Caminho para pip no venv
|
||||
const pipBin =
|
||||
process.platform === "win32"
|
||||
? path.join(venvPath, "Scripts", "pip.exe")
|
||||
: path.join(venvPath, "bin", "pip");
|
||||
|
||||
const pythonBin =
|
||||
process.platform === "win32"
|
||||
? path.join(venvPath, "Scripts", "python.exe")
|
||||
: path.join(venvPath, "bin", "python");
|
||||
|
||||
try {
|
||||
const result = await spawnWithProgress(pipBin, ["install", packageName], {
|
||||
cwd: venvPath,
|
||||
timeout: MCP_CONFIG.installTimeout,
|
||||
onOutput: (data, isError) => {
|
||||
if (config.enableLogs) {
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: `pip: ${data.substring(0, 100)}`,
|
||||
percentage: 60,
|
||||
logs: [data],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`pip install failed: ${result.stderr || result.stdout}`
|
||||
);
|
||||
}
|
||||
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: "Python package installed successfully",
|
||||
percentage: 80,
|
||||
logs: config.enableLogs ? ["✓ Installation completed"] : undefined,
|
||||
});
|
||||
|
||||
// Extrair módulo Python do nome do pacote (ex: mcp-server-git -> mcp_server_git)
|
||||
const moduleName = packageName.replace(/-/g, "_");
|
||||
|
||||
const environment: MCPEnvironment = {
|
||||
type: "python",
|
||||
path: venvPath,
|
||||
executable: pythonBin,
|
||||
args: ["-m", moduleName],
|
||||
};
|
||||
|
||||
return environment;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Python installation failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instala servidor Rust (via git clone + cargo build)
|
||||
*/
|
||||
private static async installRust(
|
||||
config: InstallationConfig,
|
||||
onProgress: (progress: InstallationProgress) => void,
|
||||
dependencies: any
|
||||
): Promise<MCPEnvironment> {
|
||||
if (!dependencies.cargo || !dependencies.git) {
|
||||
throw new Error("Cargo and Git are not installed on this system");
|
||||
}
|
||||
|
||||
if (!config.repositoryUrl) {
|
||||
throw new Error("Repository URL is required for Rust installation");
|
||||
}
|
||||
|
||||
onProgress({
|
||||
status: "downloading" as MCPInstallationStatus,
|
||||
message: "Cloning Rust repository...",
|
||||
percentage: 20,
|
||||
logs: config.enableLogs
|
||||
? [`Cloning ${config.repositoryUrl}...`]
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Garantir diretório rust existe
|
||||
await MCPEnvironmentService.ensureEnvironmentDirectory("rust");
|
||||
|
||||
const projectPath = path.join(MCP_CONFIG.rustDir, config.mcpId);
|
||||
|
||||
try {
|
||||
// Clonar repositório
|
||||
const cloneResult = await spawnWithProgress(
|
||||
"git",
|
||||
["clone", config.repositoryUrl, projectPath],
|
||||
{
|
||||
cwd: MCP_CONFIG.rustDir,
|
||||
timeout: MCP_CONFIG.installTimeout,
|
||||
onOutput: (data) => {
|
||||
if (config.enableLogs) {
|
||||
onProgress({
|
||||
status: "downloading" as MCPInstallationStatus,
|
||||
message: `git: ${data.substring(0, 100)}`,
|
||||
percentage: 30,
|
||||
logs: [data],
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (cloneResult.exitCode !== 0) {
|
||||
throw new Error(`git clone failed: ${cloneResult.stderr}`);
|
||||
}
|
||||
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: "Building Rust project...",
|
||||
percentage: 40,
|
||||
logs: config.enableLogs ? ["Building with cargo..."] : undefined,
|
||||
});
|
||||
|
||||
// Compilar com cargo
|
||||
const buildResult = await spawnWithProgress(
|
||||
"cargo",
|
||||
["build", "--release"],
|
||||
{
|
||||
cwd: projectPath,
|
||||
timeout: MCP_CONFIG.installTimeout,
|
||||
onOutput: (data, isError) => {
|
||||
if (config.enableLogs) {
|
||||
let percentage = 50;
|
||||
if (data.includes("Compiling")) percentage = 60;
|
||||
if (data.includes("Finished")) percentage = 80;
|
||||
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: `cargo: ${data.substring(0, 100)}`,
|
||||
percentage,
|
||||
logs: [data],
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (buildResult.exitCode !== 0) {
|
||||
throw new Error(`cargo build failed: ${buildResult.stderr}`);
|
||||
}
|
||||
|
||||
onProgress({
|
||||
status: "installing" as MCPInstallationStatus,
|
||||
message: "Rust project built successfully",
|
||||
percentage: 80,
|
||||
logs: config.enableLogs ? ["✓ Build completed"] : undefined,
|
||||
});
|
||||
|
||||
// Determinar nome do binário
|
||||
const binaryName = config.packageName || config.mcpId;
|
||||
const binaryPath = path.join(
|
||||
projectPath,
|
||||
"target",
|
||||
"release",
|
||||
process.platform === "win32" ? `${binaryName}.exe` : binaryName
|
||||
);
|
||||
|
||||
const environment: MCPEnvironment = {
|
||||
type: "rust",
|
||||
path: projectPath,
|
||||
executable: binaryPath,
|
||||
args: [],
|
||||
};
|
||||
|
||||
return environment;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Rust installation failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
/**
|
||||
* Serviço de validação de protocolo MCP (JSON-RPC)
|
||||
*/
|
||||
|
||||
import { spawn } from "child_process";
|
||||
import type {
|
||||
MCPEnvironment,
|
||||
ValidationResult,
|
||||
} from "@/lib/types/mcp-installer";
|
||||
import { MCP_CONFIG } from "@/lib/config/mcp";
|
||||
|
||||
export class MCPValidatorService {
|
||||
/**
|
||||
* Valida se um servidor MCP está funcionando corretamente
|
||||
* Testa o protocolo JSON-RPC do MCP
|
||||
*/
|
||||
static async validateMCPServer(
|
||||
environment: MCPEnvironment,
|
||||
config: any,
|
||||
timeoutMs: number = MCP_CONFIG.testTimeout
|
||||
): Promise<ValidationResult> {
|
||||
return new Promise((resolve) => {
|
||||
const startTime = Date.now();
|
||||
let hasResolved = false;
|
||||
|
||||
// Preparar comando e argumentos
|
||||
const command = environment.executable;
|
||||
const args = environment.args || [];
|
||||
const env = {
|
||||
...process.env,
|
||||
...environment.env,
|
||||
...(config.env || {}),
|
||||
};
|
||||
|
||||
// Para NPM, adicionar node_modules/.bin ao PATH para encontrar binários locais
|
||||
if (environment.type === "npm" && environment.path) {
|
||||
const nodeBinPath = `${environment.path}/node_modules/.bin`;
|
||||
env.PATH = `${nodeBinPath}:${env.PATH || process.env.PATH}`;
|
||||
}
|
||||
|
||||
console.log(`🧪 Testing MCP server: ${command} ${args.join(" ")}`);
|
||||
|
||||
try {
|
||||
// Spawnar processo do servidor MCP
|
||||
const serverProcess = spawn(command, args, {
|
||||
cwd: environment.path, // Executar do diretório de instalação
|
||||
env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
shell: process.platform === "win32", // Workaround para *.cmd no Windows
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const responses: any[] = [];
|
||||
let currentBuffer = "";
|
||||
|
||||
// Timeout
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
serverProcess.kill();
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Timeout after ${timeoutMs}ms. Server did not respond.`,
|
||||
});
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
// Capturar stdout (respostas JSON-RPC)
|
||||
serverProcess.stdout?.on("data", (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
stdout += text;
|
||||
currentBuffer += text;
|
||||
|
||||
// Processar respostas JSON-RPC linha por linha
|
||||
const lines = currentBuffer.split("\n");
|
||||
currentBuffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith("{")) {
|
||||
try {
|
||||
const response = JSON.parse(trimmed);
|
||||
responses.push(response);
|
||||
console.log(
|
||||
"📨 MCP Response:",
|
||||
JSON.stringify(response).substring(0, 200)
|
||||
);
|
||||
|
||||
// Verificar se recebemos resposta válida de initialize
|
||||
if (
|
||||
response.id === 1 &&
|
||||
response.result &&
|
||||
response.result.capabilities
|
||||
) {
|
||||
// Protocolo MCP detectado, agora listar ferramentas
|
||||
this.sendRequest(serverProcess, {
|
||||
jsonrpc: "2.0",
|
||||
method: "tools/list",
|
||||
id: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Verificar se recebemos lista de ferramentas
|
||||
if (response.id === 2 && response.result) {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
serverProcess.kill();
|
||||
|
||||
const tools = Array.isArray(response.result.tools)
|
||||
? response.result.tools.map((tool: any) => ({
|
||||
name: tool.name || "unnamed",
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
}))
|
||||
: [];
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
protocol: "mcp",
|
||||
tools,
|
||||
capabilities: responses[0]?.result?.capabilities,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Não é JSON válido, ignorar
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Capturar stderr
|
||||
serverProcess.stderr?.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
// Error handler
|
||||
serverProcess.on("error", (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Failed to spawn server: ${error.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Exit handler
|
||||
serverProcess.on("exit", (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
|
||||
// Se saiu muito rápido sem responder, é um erro
|
||||
const elapsed = Date.now() - startTime;
|
||||
if (elapsed < 1000) {
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Server exited quickly with code ${code}. stderr: ${stderr}`,
|
||||
});
|
||||
} else {
|
||||
// Servidor pode ter processado e saído normalmente
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Server exited with code ${code} before completing validation`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Aguardar servidor estar pronto (pequeno delay)
|
||||
setTimeout(() => {
|
||||
if (!hasResolved) {
|
||||
// Enviar requisição de initialize
|
||||
this.sendRequest(serverProcess, {
|
||||
jsonrpc: "2.0",
|
||||
method: "initialize",
|
||||
params: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: "ollahub",
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
id: 1,
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
} catch (error: any) {
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Validation error: ${error.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Envia requisição JSON-RPC para o servidor
|
||||
*/
|
||||
private static sendRequest(process: any, request: any): void {
|
||||
try {
|
||||
const requestStr = JSON.stringify(request) + "\n";
|
||||
console.log("📤 Sending MCP request:", requestStr.trim());
|
||||
process.stdin?.write(requestStr);
|
||||
} catch (error: any) {
|
||||
console.error("Failed to send request:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validação simples de servidor (apenas verifica se inicia)
|
||||
* Usado como fallback se validação JSON-RPC falhar
|
||||
*/
|
||||
static async simpleValidation(
|
||||
environment: MCPEnvironment,
|
||||
config: any,
|
||||
timeoutMs: number = 5000
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const command = environment.executable;
|
||||
const args = environment.args || [];
|
||||
const env = {
|
||||
...process.env,
|
||||
...environment.env,
|
||||
...(config.env || {}),
|
||||
};
|
||||
|
||||
// Para NPM, adicionar node_modules/.bin ao PATH para encontrar binários locais
|
||||
if (environment.type === "npm" && environment.path) {
|
||||
const nodeBinPath = `${environment.path}/node_modules/.bin`;
|
||||
env.PATH = `${nodeBinPath}:${env.PATH || process.env.PATH}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const serverProcess = spawn(command, args, {
|
||||
cwd: environment.path, // Executar do diretório de instalação
|
||||
env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
let hasResolved = false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
serverProcess.kill();
|
||||
// Se chegou ao timeout sem errar, provavelmente está rodando
|
||||
resolve({ success: true });
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
serverProcess.on("error", (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to start: ${error.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
serverProcess.on("exit", (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
if (code === 0) {
|
||||
resolve({ success: true });
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Exited with code ${code}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Validation error: ${error.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Serviço para detectar capabilities de function calling dos modelos
|
||||
*/
|
||||
|
||||
import type { ModelCapabilities } from "@/lib/types/mcp-chat";
|
||||
|
||||
/**
|
||||
* Lista de modelos com suporte nativo a function calling
|
||||
* Baseado na documentação do Ollama (atualizado em nov 2024)
|
||||
*/
|
||||
const MODELS_WITH_NATIVE_TOOLS = [
|
||||
// Llama 3.x
|
||||
/^llama3\.1/i,
|
||||
/^llama3\.2/i,
|
||||
/^llama3-/i,
|
||||
|
||||
// Mistral
|
||||
/^mistral/i,
|
||||
/^mistral-nemo/i,
|
||||
/^mixtral/i,
|
||||
|
||||
// Gemma
|
||||
/^gemma2/i,
|
||||
|
||||
// Qwen
|
||||
/^qwen2\.5/i,
|
||||
|
||||
// Command-R
|
||||
/^command-r/i,
|
||||
|
||||
// Firefunction
|
||||
/^firefunction/i,
|
||||
];
|
||||
|
||||
/**
|
||||
* Detecta capabilities de function calling de um modelo
|
||||
* @param modelName Nome do modelo (ex: "llama3.1:8b", "qwen2.5:7b")
|
||||
* @returns Capabilities do modelo
|
||||
*/
|
||||
export function detectModelCapabilities(modelName: string): ModelCapabilities {
|
||||
const supportsNativeTools = MODELS_WITH_NATIVE_TOOLS.some((pattern) =>
|
||||
pattern.test(modelName)
|
||||
);
|
||||
|
||||
return {
|
||||
modelName,
|
||||
supportsNativeTools,
|
||||
requiresPromptEngineering: !supportsNativeTools,
|
||||
toolCallFormat: supportsNativeTools ? "openai" : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se um modelo específico suporta function calling nativo
|
||||
* @param modelName Nome do modelo
|
||||
* @returns true se suporta, false caso contrário
|
||||
*/
|
||||
export function supportsNativeTools(modelName: string): boolean {
|
||||
return detectModelCapabilities(modelName).supportsNativeTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista todos os modelos conhecidos com suporte a function calling
|
||||
* @returns Array de padrões regex em string
|
||||
*/
|
||||
export function getSupportedModelsPatterns(): string[] {
|
||||
return [
|
||||
"llama3.1:*",
|
||||
"llama3.2:*",
|
||||
"mistral:*",
|
||||
"mixtral:*",
|
||||
"gemma2:*",
|
||||
"qwen2.5:*",
|
||||
"command-r:*",
|
||||
"firefunction:*",
|
||||
];
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
export const DEFAULT_SYSTEM_PROMPT = `Você é um assistente de IA especializado em desenvolvimento de software e análise técnica.
|
||||
|
||||
FORMATAÇÃO OBRIGATÓRIA:
|
||||
- Sempre use blocos de código markdown com especificação de linguagem
|
||||
- Para JavaScript/TypeScript: \`\`\`javascript ou \`\`\`typescript
|
||||
- Para Python: \`\`\`python
|
||||
- Para Bash/Shell: \`\`\`bash
|
||||
- Para SQL: \`\`\`sql
|
||||
- Para JSON: \`\`\`json
|
||||
- Para YAML: \`\`\`yaml
|
||||
- Para Docker: \`\`\`dockerfile
|
||||
- Para configurações: \`\`\`toml, \`\`\`ini, \`\`\`conf
|
||||
|
||||
ESTRUTURA DE RESPOSTAS:
|
||||
1. Explicação conceitual clara
|
||||
2. Exemplo prático em bloco de código
|
||||
3. Explicação do código quando necessário
|
||||
4. Boas práticas
|
||||
5. Possíveis problemas e soluções
|
||||
|
||||
CONTEXTO OLLAHUB:
|
||||
- Execução local via Ollama
|
||||
- Stack frequente: Next.js, React, Node.js
|
||||
- Priorize exemplos executáveis e explique comandos e configurações.`;
|
||||
|
||||
export function mergeSystemPrompt(userPrompt?: string) {
|
||||
if (!userPrompt || userPrompt.trim().length === 0)
|
||||
return DEFAULT_SYSTEM_PROMPT;
|
||||
return `${DEFAULT_SYSTEM_PROMPT}\n\nDiretivas do usuário:\n${userPrompt}`;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Tipos e interfaces para integração de MCPs no chat
|
||||
*/
|
||||
|
||||
/**
|
||||
* Representa um tool MCP com schema JSON
|
||||
*/
|
||||
export interface MCPTool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Contexto de MCPs ativos para a conversa
|
||||
*/
|
||||
export interface MCPChatContext {
|
||||
mcpId: string;
|
||||
enabled: boolean;
|
||||
tools: MCPTool[];
|
||||
config: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resultado da execução de um MCP
|
||||
*/
|
||||
export interface MCPExecutionResult {
|
||||
success: boolean;
|
||||
toolName: string;
|
||||
mcpId: string;
|
||||
result?: any;
|
||||
error?: string;
|
||||
executionTime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capabilities de function calling do modelo
|
||||
*/
|
||||
export interface ModelCapabilities {
|
||||
supportsNativeTools: boolean;
|
||||
requiresPromptEngineering: boolean;
|
||||
modelName: string;
|
||||
toolCallFormat?: "openai" | "anthropic" | "custom";
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuração de MCPs para chat
|
||||
*/
|
||||
export interface MCPChatConfig {
|
||||
alwaysActive: string[]; // MCPs sempre ativos
|
||||
neverUse: string[]; // MCPs nunca usar
|
||||
activationMode: "automatic" | "keywords" | "manual";
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent detectado na mensagem do usuário
|
||||
*/
|
||||
export interface DetectedIntent {
|
||||
type:
|
||||
| "web_search"
|
||||
| "time"
|
||||
| "weather"
|
||||
| "file"
|
||||
| "code"
|
||||
| "database"
|
||||
| "other";
|
||||
confidence: number;
|
||||
keywords: string[];
|
||||
suggestedTools: string[];
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Tipos e interfaces para o sistema de instalação MCP
|
||||
*/
|
||||
|
||||
export type PackageRegistry = "npm" | "pypi" | "cargo" | "other" | null;
|
||||
|
||||
export enum MCPInstallationStatus {
|
||||
PENDING = "pending",
|
||||
CHECKING_DEPENDENCIES = "checking_dependencies",
|
||||
DOWNLOADING = "downloading",
|
||||
INSTALLING = "installing",
|
||||
TESTING = "testing",
|
||||
READY = "ready",
|
||||
FAILED = "failed",
|
||||
}
|
||||
|
||||
export interface InstallationProgress {
|
||||
status: MCPInstallationStatus;
|
||||
message: string;
|
||||
percentage: number;
|
||||
logs?: string[];
|
||||
}
|
||||
|
||||
export interface MCPEnvironment {
|
||||
type: "npm" | "python" | "rust";
|
||||
path: string;
|
||||
executable: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
success: boolean;
|
||||
protocol: "mcp" | "unknown";
|
||||
tools: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
inputSchema?: any;
|
||||
}>;
|
||||
resources?: Array<{
|
||||
uri: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}>;
|
||||
error?: string;
|
||||
capabilities?: any;
|
||||
}
|
||||
|
||||
export interface SystemDependencies {
|
||||
node: boolean;
|
||||
npm: boolean;
|
||||
python: boolean;
|
||||
pip: boolean;
|
||||
cargo: boolean;
|
||||
git: boolean;
|
||||
}
|
||||
|
||||
export interface InstallationConfig {
|
||||
mcpId: string;
|
||||
packageName: string;
|
||||
packageRegistry: PackageRegistry;
|
||||
repositoryUrl?: string;
|
||||
config: any;
|
||||
enableLogs: boolean;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
export interface MCPProvider {
|
||||
id: string; // formato: "owner/repo"
|
||||
originalId?: string; // ID original do cache antes de transformar para owner/repo
|
||||
owner?: string; // Owner do repositório
|
||||
repo?: string; // Nome do repositório
|
||||
name: string; // mapeado de content_name
|
||||
author: string; // mapeado de publisher_id
|
||||
description: string;
|
||||
version: string;
|
||||
category:
|
||||
| "map"
|
||||
| "browser"
|
||||
| "office"
|
||||
| "search"
|
||||
| "database"
|
||||
| "finance"
|
||||
| "code"
|
||||
| "chart"
|
||||
| "payment"
|
||||
| "other";
|
||||
tags: string[]; // mapeado de content_tag_list
|
||||
rating: number; // já existe na API
|
||||
totalRatings: number; // mapeado de review_cnt
|
||||
repository?: string; // mapeado de website
|
||||
homepage?: string;
|
||||
installed: boolean; // verificar localmente
|
||||
// Novos campos da API DeepNLP
|
||||
subfield?: string; // MAP, BROWSER, etc
|
||||
field?: string; // sempre "MCP SERVER"
|
||||
config?: any[]; // configuração JSON do servidor
|
||||
tools?: MCPTool[]; // schemas das ferramentas
|
||||
}
|
||||
|
||||
export interface MCPCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface MCPInstallationStatus {
|
||||
success: boolean;
|
||||
message: string;
|
||||
mcpId: string;
|
||||
}
|
||||
|
||||
export interface MCPTool {
|
||||
name: string;
|
||||
description: string;
|
||||
input_schema: {
|
||||
type: string;
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// Tipos relacionados a busca/marketplace foram removidos
|
||||
|
||||
export interface MCPTestResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
responseTime?: number; // em ms
|
||||
serverVersion?: string;
|
||||
availableTools?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface MCPTestRequest {
|
||||
mcpId: string;
|
||||
}
|
||||
|
||||
// Metadados específicos de PulseMCP removidos
|
||||
@@ -1,62 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export type SavedRemoteModel = {
|
||||
name: string; // full ref, e.g., owner/model:tag or model:tag
|
||||
description?: string;
|
||||
url?: string;
|
||||
tags_count?: number;
|
||||
installed?: boolean;
|
||||
lastUsedAt?: string; // ISO
|
||||
};
|
||||
|
||||
type SavedRemoteFile = {
|
||||
models: SavedRemoteModel[];
|
||||
};
|
||||
|
||||
function getFilePath() {
|
||||
return path.join(process.cwd(), "database", "user-remote-models.json");
|
||||
}
|
||||
|
||||
async function ensureFile(): Promise<void> {
|
||||
const file = getFilePath();
|
||||
try {
|
||||
await fs.access(file);
|
||||
} catch {
|
||||
const initial: SavedRemoteFile = { models: [] };
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
await fs.writeFile(file, JSON.stringify(initial, null, 2), "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSavedRemoteModels(): Promise<SavedRemoteModel[]> {
|
||||
await ensureFile();
|
||||
const raw = await fs.readFile(getFilePath(), "utf-8");
|
||||
try {
|
||||
const json = JSON.parse(raw) as SavedRemoteFile;
|
||||
return Array.isArray(json.models) ? json.models : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function addSavedRemoteModel(entry: SavedRemoteModel): Promise<void> {
|
||||
await ensureFile();
|
||||
const current = await getSavedRemoteModels();
|
||||
const nowIso = new Date().toISOString();
|
||||
const idx = current.findIndex((m) => m.name === entry.name);
|
||||
const merged: SavedRemoteModel = {
|
||||
...entry,
|
||||
installed: entry.installed ?? false,
|
||||
lastUsedAt: entry.lastUsedAt ?? nowIso,
|
||||
};
|
||||
if (idx >= 0) {
|
||||
current[idx] = { ...current[idx], ...merged, lastUsedAt: nowIso };
|
||||
} else {
|
||||
current.unshift(merged);
|
||||
}
|
||||
const toWrite: SavedRemoteFile = { models: current.slice(0, 500) };
|
||||
await fs.writeFile(getFilePath(), JSON.stringify(toWrite, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
/**
|
||||
* Utilitários para operações de sistema
|
||||
*/
|
||||
|
||||
import { exec, spawn } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Verifica se um comando existe no sistema
|
||||
*/
|
||||
export async function commandExists(command: string): Promise<boolean> {
|
||||
try {
|
||||
const isWindows = process.platform === "win32";
|
||||
const checkCommand = isWindows ? "where" : "which";
|
||||
|
||||
await execAsync(`${checkCommand} ${command}`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa um comando e retorna stdout/stderr
|
||||
*/
|
||||
export async function execCommand(
|
||||
cmd: string,
|
||||
args: string[],
|
||||
options?: {
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
timeout?: number;
|
||||
}
|
||||
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
||||
try {
|
||||
const fullCommand = `${cmd} ${args.join(" ")}`;
|
||||
const { stdout, stderr } = await execAsync(fullCommand, {
|
||||
cwd: options?.cwd,
|
||||
env: options?.env ? { ...process.env, ...options.env } : process.env,
|
||||
timeout: options?.timeout || 300000, // 5 minutos default
|
||||
});
|
||||
|
||||
return { stdout, stderr, exitCode: 0 };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
stdout: error.stdout || "",
|
||||
stderr: error.stderr || error.message,
|
||||
exitCode: error.code || 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawna um processo e captura output com progresso
|
||||
*/
|
||||
export async function spawnWithProgress(
|
||||
cmd: string,
|
||||
args: string[],
|
||||
options: {
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
onOutput?: (data: string, isError: boolean) => void;
|
||||
timeout?: number;
|
||||
}
|
||||
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let hasTimedOut = false;
|
||||
|
||||
const childProcess = spawn(cmd, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env ? { ...process.env, ...options.env } : process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
// Timeout handler
|
||||
const timeout = options.timeout || 300000; // 5 minutos default
|
||||
const timeoutId = setTimeout(() => {
|
||||
hasTimedOut = true;
|
||||
childProcess.kill("SIGTERM");
|
||||
|
||||
// Force kill após 5 segundos
|
||||
setTimeout(() => {
|
||||
if (!childProcess.killed) {
|
||||
childProcess.kill("SIGKILL");
|
||||
}
|
||||
}, 5000);
|
||||
}, timeout);
|
||||
|
||||
// Capturar stdout
|
||||
childProcess.stdout?.on("data", (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
stdout += text;
|
||||
if (options.onOutput) {
|
||||
options.onOutput(text, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Capturar stderr
|
||||
childProcess.stderr?.on("data", (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
stderr += text;
|
||||
if (options.onOutput) {
|
||||
options.onOutput(text, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Error handler
|
||||
childProcess.on("error", (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error(`Failed to spawn process: ${error.message}`));
|
||||
});
|
||||
|
||||
// Exit handler
|
||||
childProcess.on("close", (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (hasTimedOut) {
|
||||
reject(
|
||||
new Error(
|
||||
`Process timed out after ${timeout}ms. stdout: ${stdout}, stderr: ${stderr}`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
resolve({
|
||||
exitCode: code || 0,
|
||||
stdout,
|
||||
stderr,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza nome de pacote para evitar command injection
|
||||
* Suporta formatos: @scope/pkg, github:user/repo, git+https://..., etc.
|
||||
*/
|
||||
export function sanitizePackageName(packageName: string): string {
|
||||
// URLs git+https:// devem ser preservadas completamente (são seguras)
|
||||
if (
|
||||
packageName.startsWith("git+https://") ||
|
||||
packageName.startsWith("git+http://")
|
||||
) {
|
||||
// Apenas validar que parece uma URL válida
|
||||
return packageName;
|
||||
}
|
||||
|
||||
// Remove caracteres perigosos, mantém alphanumeric, @, :, +, /, -, _, .
|
||||
// Suporta: @scope/pkg, github:user/repo
|
||||
return packageName.replace(/[^a-zA-Z0-9@:+/\-_.]/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se um caminho está dentro de um diretório base (previne path traversal)
|
||||
*/
|
||||
export function isPathSafe(basePath: string, targetPath: string): boolean {
|
||||
const path = require("path");
|
||||
const resolvedBase = path.resolve(basePath);
|
||||
const resolvedTarget = path.resolve(targetPath);
|
||||
|
||||
return resolvedTarget.startsWith(resolvedBase);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { generateEmbedding } from "./embeddings";
|
||||
import { getDatabase } from "@/database/client";
|
||||
import { EmbeddingRepository } from "@/database/repositories/embedding";
|
||||
|
||||
export async function semanticSearch(query: string, limit: number = 5) {
|
||||
try {
|
||||
const queryVector = await generateEmbedding(query);
|
||||
const db = getDatabase();
|
||||
const embeddingRepo = new EmbeddingRepository(db);
|
||||
|
||||
return await embeddingRepo.searchSimilar(queryVector, limit);
|
||||
} catch (error) {
|
||||
console.error("Erro na busca semântica:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function addMessageEmbedding(
|
||||
messageId: string,
|
||||
content: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const vector = await generateEmbedding(content);
|
||||
const db = getDatabase();
|
||||
const embeddingRepo = new EmbeddingRepository(db);
|
||||
|
||||
await embeddingRepo.create(messageId, vector);
|
||||
} catch (error) {
|
||||
console.error("Erro ao adicionar embedding:", error);
|
||||
// Não falhar silenciosamente - embedding é opcional
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user