import dotenv from "dotenv"; import fs from "node:fs"; dotenv.config(); function resolveDbSsl() { const caPath = process.env.DB_SSL_CA; const rejectUnauthorizedEnv = process.env.DB_SSL_REJECT_UNAUTHORIZED; if (!caPath && rejectUnauthorizedEnv === undefined) return undefined; return { rejectUnauthorized: rejectUnauthorizedEnv !== "false", ...(caPath ? { ca: fs.readFileSync(caPath, "utf8") } : {}) }; } export const config = { port: Number(process.env.PORT ?? 3001), corsOrigin: process.env.CORS_ORIGIN ?? "http://localhost:5173", apiKey: process.env.API_KEY ?? "", auth: { mode: process.env.AUTH_MODE ?? "none" }, http: { defaultTimeoutMs: Number(process.env.HTTP_DEFAULT_TIMEOUT_MS ?? 15_000) }, db: { host: process.env.DB_HOST, user: process.env.DB_USER, port: process.env.DB_PORT ? Number(process.env.DB_PORT) : undefined, password: process.env.DB_PASS, schema: process.env.DB_SCHEMA, charset: "utf8mb4", ssl: resolveDbSsl() }, jwt: { secret: process.env.JWT_SECRET ?? "", issuer: process.env.JWT_ISSUER ?? "oraculo-api", accessTtlSeconds: Number(process.env.JWT_ACCESS_TTL_SECONDS ?? 900), refreshTtlSeconds: Number(process.env.JWT_REFRESH_TTL_SECONDS ?? 60 * 60 * 24 * 30), refreshTtlShortSeconds: Number(process.env.JWT_REFRESH_TTL_SHORT_SECONDS ?? 60 * 60 * 24) }, qdrant: { url: process.env.QDRANT_URL ?? "http://localhost:6333", apiKey: process.env.QDRANT_API_KEY ?? "", collection: process.env.QDRANT_COLLECTION ?? "oraculo_docs" }, ollama: { url: process.env.OLLAMA_URL ?? "http://localhost:11434", embeddingsModel: process.env.OLLAMA_EMBEDDINGS_MODEL ?? "nomic-embed-text", embedQueryPrefix: process.env.OLLAMA_EMBED_QUERY_PREFIX ?? "", embedPassagePrefix: process.env.OLLAMA_EMBED_PASSAGE_PREFIX ?? "", chatModel: process.env.OLLAMA_CHAT_MODEL ?? "llama3.1", visionModel: process.env.OLLAMA_VISION_MODEL ?? "llava:latest", timeoutMs: Number(process.env.OLLAMA_TIMEOUT_MS ?? 180_000), streamIdleTimeoutMs: Number(process.env.OLLAMA_STREAM_IDLE_TIMEOUT_MS ?? 60_000) }, rag: { topK: Number(process.env.RAG_TOP_K ?? 8), minScore: Number(process.env.RAG_MIN_SCORE ?? 0.40), chunkSize: Number(process.env.RAG_CHUNK_SIZE ?? 900), chunkOverlap: Number(process.env.RAG_CHUNK_OVERLAP ?? 150), queryRewrite: process.env.RAG_QUERY_REWRITE === "true", historyLimit: Number(process.env.RAG_HISTORY_LIMIT ?? 6), rerank: process.env.RAG_RERANK === "true", rerankTopN: Number(process.env.RAG_RERANK_TOP_N ?? process.env.RAG_TOP_K ?? 8), hyde: process.env.RAG_HYDE === "true" }, llm: { temperature: Number(process.env.LLM_TEMPERATURE ?? 0.2), topP: Number(process.env.LLM_TOP_P ?? 0.9), numPredict: Number(process.env.LLM_NUM_PREDICT ?? 4096), repeatPenalty: Number(process.env.LLM_REPEAT_PENALTY ?? 1.1), numCtx: Number(process.env.LLM_NUM_CTX ?? 8192), atendimentosNumCtx: Number(process.env.LLM_ATENDIMENTOS_NUM_CTX ?? 32768), auxNumPredict: Number(process.env.LLM_AUX_NUM_PREDICT ?? 400) }, rateLimit: { perMinute: Number(process.env.RATE_LIMIT_PER_MINUTE ?? 0) }, ifbot: { baseUrl: process.env.IFBOT_BASE_URL ?? "https://star.ifbot.com.br/api", mediaBaseUrl: (process.env.IFBOT_MEDIA_BASE_URL ?? (process.env.IFBOT_BASE_URL ?? "https://star.ifbot.com.br/api").replace(/\/api\/?$/, "")).replace(/\/+$/, ""), token: process.env.IFBOT_TOKEN ?? "", authHeader: process.env.IFBOT_AUTH_HEADER ?? "Authorization", syncIntervalMinutes: Number(process.env.ATENDIMENTOS_SYNC_INTERVAL_MINUTES ?? 30) }, avaliacao: { model: process.env.OLLAMA_JUDGE_MODEL ?? "", intervalMinutes: Number(process.env.ATENDIMENTOS_AVALIACAO_INTERVAL_MINUTES ?? 60), batchSize: Number(process.env.ATENDIMENTOS_AVALIACAO_BATCH ?? 10), maxChars: Number(process.env.ATENDIMENTOS_AVALIACAO_MAX_CHARS ?? 12000) }, atendimentosRag: { collection: process.env.ATENDIMENTOS_RAG_COLLECTION ?? "atendimentos_rag", intervalMinutes: Number(process.env.ATENDIMENTOS_RAG_INTERVAL_MINUTES ?? 60), batchSize: Number(process.env.ATENDIMENTOS_RAG_BATCH ?? 10), chunkSize: Number(process.env.ATENDIMENTOS_RAG_CHUNK_SIZE ?? 1800), chunkOverlap: Number(process.env.ATENDIMENTOS_RAG_CHUNK_OVERLAP ?? 200), topK: Number(process.env.ATENDIMENTOS_RAG_TOP_K ?? 20), minScore: Number(process.env.ATENDIMENTOS_RAG_MIN_SCORE ?? 0.5), searchEnabled: process.env.ATENDIMENTOS_RAG_SEARCH_ENABLED === "true", hyde: process.env.ATENDIMENTOS_RAG_HYDE === "true", rerank: process.env.ATENDIMENTOS_RAG_RERANK !== "false", rerankTopN: Number(process.env.ATENDIMENTOS_RAG_RERANK_TOP_N ?? 10) }, atendenteSugestao: { model: process.env.OLLAMA_ATENDENTE_MODEL ?? "star-atendente", historyLimit: Number(process.env.ATENDENTE_SUGESTAO_HISTORY_LIMIT ?? 20), maxChars: Number(process.env.ATENDENTE_SUGESTAO_MAX_CHARS ?? 8000), ragEnabled: process.env.ATENDENTE_SUGESTAO_RAG_ENABLED !== "false", ragTopK: Number(process.env.ATENDENTE_SUGESTAO_RAG_TOP_K ?? 3), ragMinScore: Number(process.env.ATENDENTE_SUGESTAO_RAG_MIN_SCORE ?? process.env.ATENDIMENTOS_RAG_MIN_SCORE ?? 0.5), ragMaxCharsPerHit: Number(process.env.ATENDENTE_SUGESTAO_RAG_MAX_CHARS_POR_HIT ?? 600) }, whatsapp: { apiVersion: process.env.WHATSAPP_API_VERSION || "v21.0", phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID ?? "", token: process.env.WHATSAPP_TOKEN ?? "", verifyToken: process.env.WHATSAPP_VERIFY_TOKEN ?? "", appSecret: process.env.WHATSAPP_APP_SECRET ?? "", businessAccountId: process.env.WHATSAPP_BUSINESS_ACCOUNT_ID ?? "" }, ixcIntegradorSeed: { baseUrl: process.env.IXC_INTEGRADOR_BASE_URL ?? "", login: process.env.IXC_INTEGRADOR_LOGIN ?? "", senha: process.env.IXC_INTEGRADOR_SENHA ?? "" }, whisper: { enabled: process.env.WHISPER_TRANSCRICAO_ENABLED === "true", binaryPath: process.env.WHISPER_BINARY_PATH ?? "whisper-cli", ffmpegPath: process.env.WHISPER_FFMPEG_PATH ?? "ffmpeg", modelPath: process.env.WHISPER_MODEL_PATH ?? "whisper_models/ggml-small.bin", vadModelPath: process.env.WHISPER_VAD_MODEL_PATH ?? "whisper_models/ggml-silero-vad.bin", timeoutMs: Number(process.env.WHISPER_TIMEOUT_MS ?? 60_000) } }; function validateConfig(cfg) { const dbRequired = { host: "DB_HOST", user: "DB_USER", password: "DB_PASS", schema: "DB_SCHEMA" }; for (const [key, envName] of Object.entries(dbRequired)) { if (!cfg.db[key]) throw new Error(`[config] ${envName} é obrigatório`); } if (cfg.auth.mode === "jwt" && !cfg.jwt.secret) { throw new Error("[config] JWT_SECRET não pode ser vazio quando AUTH_MODE=jwt"); } if (cfg.rateLimit.perMinute === 0) { console.warn("[config] RATE_LIMIT_PER_MINUTE=0 — rate limiting desabilitado"); } if (!cfg.whatsapp.token || !cfg.whatsapp.phoneNumberId) { console.warn("[config] WHATSAPP_TOKEN/WHATSAPP_PHONE_NUMBER_ID não configurados — envio via WhatsApp ficará desabilitado (whatsapp_nao_conectado)"); } if (!cfg.whatsapp.verifyToken) { console.warn("[config] WHATSAPP_VERIFY_TOKEN não configurado — a verificação do webhook (GET /webhooks/whatsapp) sempre retornará 403"); } if (!cfg.whatsapp.appSecret) { console.warn("[config] WHATSAPP_APP_SECRET vazio — assinatura X-Hub-Signature-256 do webhook NÃO será validada"); } } validateConfig(config);