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" }, 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) }, 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 ?? 32768) }, rateLimit: { perMinute: Number(process.env.RATE_LIMIT_PER_MINUTE ?? 0) }, ifbot: { baseUrl: process.env.IFBOT_BASE_URL ?? "https://star.ifbot.com.br/api", // arquivos de mídia (/data/img, /data/aud) são servidos na raiz do domínio, fora do /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: { // modelo usado como juiz; vazio usa o chatModel do Ollama 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) } }; 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"); } } validateConfig(config);