fix(ai): update gemini fallback models from 1.5-flash and 2.5-flash-lite to 2.0-flash
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m5s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m5s
Details
This commit is contained in:
parent
45be343a2b
commit
eefcbcc671
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Dados de Acesso ao Banco de Dados PostgreSQL (VPS)
|
||||||
|
|
||||||
|
Abaixo estão descritas as credenciais e parâmetros de conexão para o banco de dados do **EduManager** hospedado na VPS.
|
||||||
|
|
||||||
|
## 🔑 Credenciais do Banco
|
||||||
|
|
||||||
|
* **Host (IP da VPS):** `150.230.87.131`
|
||||||
|
* **Porta padrão:** `5432`
|
||||||
|
* **Banco de Dados (Database):** `edumanager`
|
||||||
|
* **Usuário (Username):** `edumanager`
|
||||||
|
* **Senha (Password):** `EduManager2026!Seguro`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Strings de Conexão
|
||||||
|
|
||||||
|
### URI Padrão (PostgreSQL)
|
||||||
|
```text
|
||||||
|
postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager
|
||||||
|
```
|
||||||
|
|
||||||
|
### JDBC Connection String
|
||||||
|
```text
|
||||||
|
jdbc:postgresql://150.230.87.131:5432/edumanager?user=edumanager&password=EduManager2026!Seguro
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Configuração do Servidor MCP (Model Context Protocol)
|
||||||
|
|
||||||
|
O acesso MCP está configurado no arquivo global do seu IDE em `C:\Users\Sidney\.gemini\antigravity-ide\mcp_config.json` com o seguinte formato:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"edumanager-db-vps": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/server-postgres",
|
||||||
|
"postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Relatório de Segurança do App — EduManager & Portal do Aluno
|
||||||
|
|
||||||
|
## 1. Segurança no Portal do Aluno (`portal`)
|
||||||
|
O Portal do Aluno possui uma estrutura de segurança robusta baseada em JWT e middlewares de autenticação.
|
||||||
|
|
||||||
|
* **Mecanismo de Autenticação (Login):**
|
||||||
|
* O endpoint `POST /api/portal/login` em [server.selfhosted.js](file:///c:/Users/Sidney/Documents/edumanagerpro2/portal/server.selfhosted.js#L106-L180) gerencia o acesso do aluno. Ele valida se o aluno existe no banco de dados através da busca pela matrícula (`numero_matricula`).
|
||||||
|
* **Verificação de Senha:** A comparação de senha é feita em texto plano diretamente contra a coluna `senha_portal` do banco de dados PostgreSQL. Se o aluno não tiver uma senha definida (`senha_portal` nula ou vazia), o sistema utiliza como fallback de senha **os primeiros 6 dígitos do CPF** (removendo a formatação), ou uma string vazia caso o CPF também não esteja disponível.
|
||||||
|
* **Validação de Status:** Se a conta do aluno estiver inativa no banco (`status !== 'active'`), o login é rejeitado com o código HTTP `403 Forbidden`.
|
||||||
|
* **Geração e Persistência de Sessão:**
|
||||||
|
* Ao autenticar com sucesso, o servidor assina um token **JWT** (JSON Web Token) com expiração de **7 dias** (`expiresIn: '7d'`), contendo informações não sensíveis no payload (`studentId`, `enrollmentNumber` e `name`).
|
||||||
|
* No frontend ([AuthContext.tsx](file:///c:/Users/Sidney/Documents/edumanagerpro2/portal/src/context/AuthContext.tsx#L126-L127)), esse JWT é guardado no `localStorage` do navegador sob a chave `portal_token`, juntamente com os dados do usuário em `portal_user`.
|
||||||
|
* **Proteção de Rotas (Backend API):**
|
||||||
|
* Foi implementado um middleware de segurança centralizado no backend chamado [authMiddleware](file:///c:/Users/Sidney/Documents/edumanagerpro2/portal/server.selfhosted.js#L86-L99).
|
||||||
|
* Esse middleware intercepta as requisições, extrai o JWT do cabeçalho `Authorization: Bearer <token>`, e faz a validação da assinatura utilizando a variável de ambiente `JWT_SECRET`. Se o token for inválido ou estiver expirado, ele retorna imediatamente `401 Unauthorized`.
|
||||||
|
* **Todas as rotas sensíveis** do aluno (`/api/portal/me`, `/api/portal/financeiro`, `/api/portal/boletos`, `/api/portal/notas`, `/api/portal/frequencia`, `/api/portal/contratos`, `/api/portal/avaliacoes`, `/api/portal/alterar-senha`, etc.) estão devidamente protegidas pelo `authMiddleware` no backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Segurança no EduManager (Painel Administrativo — `manager`)
|
||||||
|
A arquitetura de segurança do Painel Administrativo possui particularidades importantes. Enquanto o frontend gerencia e restrinja as exibições com base na sessão local, o backend das APIs administrativas do Manager adota uma abordagem menos restritiva de verificação.
|
||||||
|
|
||||||
|
* **Autenticação no Frontend:**
|
||||||
|
* O painel administrativo restringe o acesso aos componentes da interface gerenciando o estado de login através do [index.tsx](file:///c:/Users/Sidney/Documents/edumanagerpro2/manager/index.tsx#L55-L60).
|
||||||
|
* As sessões administrativas ativas são salvas no `localStorage` sob a chave `edumanager_session` contendo o objeto de perfil do usuário.
|
||||||
|
* **Mecanismo de Login:**
|
||||||
|
* O endpoint `POST /api/auth/login` em [server.selfhosted.js](file:///c:/Users/Sidney/Documents/edumanagerpro2/manager/server.selfhosted.js#L439-L467) pesquisa o usuário na tabela `usuarios` do Postgres pelo campo `username`.
|
||||||
|
* Assim como no portal, a comparação de senhas ocorre em texto plano (`user.password !== password`).
|
||||||
|
* Se as credenciais forem válidas, o servidor gera um JWT contendo `{ userId, username, role }` assinado com o `JWT_SECRET` com validade de **24 horas**.
|
||||||
|
* **Exposição de Rotas do Backend (API Administrativa aberta):**
|
||||||
|
* **Ponto Crítico:** Diferente do Portal do Aluno, no backend do Painel Administrativo ([server.selfhosted.js](file:///c:/Users/Sidney/Documents/edumanagerpro2/manager/server.selfhosted.js)), as APIs de gerenciamento de dados principais (`/api/school-data`, `/api/alunos`, `/api/turmas`, `/api/cobrancas`, etc.) **não implementam uma verificação ativa de JWT (Authorization Middleware)**.
|
||||||
|
* O frontend ([dbService.ts](file:///c:/Users/Sidney/Documents/edumanagerpro2/manager/services/dbService.ts#L195-L264)) faz chamadas comuns via `fetch` sem incluir o cabeçalho `Authorization: Bearer <token>`. Como consequência, as rotas do backend do Manager estão abertas para requisições diretas de qualquer cliente que conheça os caminhos das rotas, confiando exclusivamente em isolamento de rede ou segurança por obscuridade.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Armazenamento de Senhas e Segurança de Dados (PostgreSQL)
|
||||||
|
A persistência do banco de dados PostgreSQL 15 local segue regras estritas estabelecidas em [schema.sql](file:///c:/Users/Sidney/Documents/edumanagerpro2/schema.sql):
|
||||||
|
|
||||||
|
* **Senhas em Texto Claro (Plain Text):**
|
||||||
|
* As senhas dos usuários administrativos (tabela `usuarios`) e dos alunos (tabela `alunos` no campo `senha_portal`) são persistidas no banco sem uso de hashing unidirecional (como *bcrypt*, *argon2* ou *scrypt*). Elas são guardadas e comparadas de forma literal.
|
||||||
|
* **Usuário Padrão Inicial:**
|
||||||
|
* O script SQL inicial insere automaticamente o usuário administrador padrão com login `admin` e senha `admin` (`default-admin`).
|
||||||
|
* **Integridade Biométrica (Anti-Sobrescrita):**
|
||||||
|
* Conforme as regras do `GEMINI.md`, a tabela de alunos armazena dados de biometria facial na coluna `face_descriptor` (do tipo `JSONB`). As rotas de sincronização de dados protegem essa coluna usando instruções `COALESCE` no `ON CONFLICT` para evitar que biometrias e fotos válidas sejam sobrescritas com valores nulos por sincronizações vindas do JSON legado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Segurança no Upload e Armazenamento de Mídia (MinIO S3)
|
||||||
|
* **Isolamento de Credenciais:**
|
||||||
|
* Todas as credenciais de serviços de terceiros e storage (como `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `ASAAS_API_KEY`, `DATABASE_URL` e `JWT_SECRET`) estão isoladas no arquivo `.env` e injetadas pelo container Docker. O frontend não tem acesso a essas variáveis, evitando vazamento de credenciais privadas.
|
||||||
|
* **Proxy de Mídia Relativo:**
|
||||||
|
* Tanto o Manager quanto o Portal servem mídias (fotos de alunos, anexos de justificativas de falta, comprovantes de pagamento e logotipos) através de uma rota dedicada de proxy interno `/storage/:bucket/:key` (exemplo: [server.selfhosted.js](file:///c:/Users/Sidney/Documents/edumanagerpro2/manager/server.selfhosted.js#L416-L431)).
|
||||||
|
* Este proxy acessa internamente o MinIO usando o SDK da AWS e transfere os arquivos por fluxo de dados (`data.Body.pipe(res)`), ocultando as URLs físicas e as credenciais internas do storage.
|
||||||
|
* Essas rotas de proxy **são públicas**, ou seja, não validam o JWT do aluno ou administrador para entregar a imagem solicitada, garantindo que o carregamento das mídias em tags `<img>` ocorra sem interrupções por CORS ou autenticação.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Principais Pontos de Atenção e Recomendações
|
||||||
|
Com base no exposto, identifico três pontos sensíveis na arquitetura de segurança atual para seu conhecimento:
|
||||||
|
|
||||||
|
1. **Exposição de API Administrativa:** O backend do EduManager confia no frontend para ocultar as ações, mas as rotas `/api/school-data` (que lê e escreve toda a base de dados do sistema, incluindo dados financeiros do Asaas) estão abertas e expostas sem validação de tokens JWT no backend do Express.
|
||||||
|
2. **Senhas sem Hash:** O armazenamento e validação de senhas em texto plano tanto na tabela `usuarios` quanto na tabela `alunos` representa uma vulnerabilidade em caso de vazamento físico ou acesso não autorizado ao banco de dados.
|
||||||
|
3. **Senha Padrão Ativa:** Recomenda-se alterar a senha do usuário padrão `admin` criada no banco assim que possível caso esteja em ambiente de produção.
|
||||||
|
|
@ -34,7 +34,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
openaiKey: data.aiConfig?.openaiKey ? '********' : '',
|
openaiKey: data.aiConfig?.openaiKey ? '********' : '',
|
||||||
claudeKey: data.aiConfig?.claudeKey ? '********' : '',
|
claudeKey: data.aiConfig?.claudeKey ? '********' : '',
|
||||||
openrouterKey: data.aiConfig?.openrouterKey ? '********' : '',
|
openrouterKey: data.aiConfig?.openrouterKey ? '********' : '',
|
||||||
geminiModel: data.aiConfig?.geminiModel || 'gemini-1.5-flash',
|
geminiModel: data.aiConfig?.geminiModel || 'gemini-2.0-flash',
|
||||||
openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini',
|
openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini',
|
||||||
claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022',
|
claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022',
|
||||||
openrouterModel: data.aiConfig?.openrouterModel || 'google/gemini-2.5-flash',
|
openrouterModel: data.aiConfig?.openrouterModel || 'google/gemini-2.5-flash',
|
||||||
|
|
@ -50,7 +50,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
openrouter: any[];
|
openrouter: any[];
|
||||||
}>({
|
}>({
|
||||||
gemini: [
|
gemini: [
|
||||||
{ id: 'gemini-1.5-flash', name: 'gemini-1.5-flash (Mais Rápido & Gratuito)' },
|
{ id: 'gemini-2.0-flash', name: 'gemini-2.0-flash (Mais Rápido & Gratuito)' },
|
||||||
{ id: 'gemini-1.5-pro', name: 'gemini-1.5-pro (Multimodal)' },
|
{ id: 'gemini-1.5-pro', name: 'gemini-1.5-pro (Multimodal)' },
|
||||||
{ id: 'gemini-2.0-flash-exp', name: 'gemini-2.0-flash-exp (Experimental)' },
|
{ id: 'gemini-2.0-flash-exp', name: 'gemini-2.0-flash-exp (Experimental)' },
|
||||||
{ id: 'gemini-2.5-flash', name: 'gemini-2.5-flash (Se ativo na sua conta)' }
|
{ id: 'gemini-2.5-flash', name: 'gemini-2.5-flash (Se ativo na sua conta)' }
|
||||||
|
|
@ -87,7 +87,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
openaiKey: data.aiConfig?.openaiKey ? '********' : '',
|
openaiKey: data.aiConfig?.openaiKey ? '********' : '',
|
||||||
claudeKey: data.aiConfig?.claudeKey ? '********' : '',
|
claudeKey: data.aiConfig?.claudeKey ? '********' : '',
|
||||||
openrouterKey: data.aiConfig?.openrouterKey ? '********' : '',
|
openrouterKey: data.aiConfig?.openrouterKey ? '********' : '',
|
||||||
geminiModel: data.aiConfig?.geminiModel || 'gemini-1.5-flash',
|
geminiModel: data.aiConfig?.geminiModel || 'gemini-2.0-flash',
|
||||||
openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini',
|
openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini',
|
||||||
claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022',
|
claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022',
|
||||||
openrouterModel: data.aiConfig?.openrouterModel || 'google/gemini-2.5-flash',
|
openrouterModel: data.aiConfig?.openrouterModel || 'google/gemini-2.5-flash',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import pg from 'pg';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager';
|
||||||
|
const pool = new pg.Pool({ connectionString: DATABASE_URL });
|
||||||
|
|
||||||
|
async function recover() {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
console.log('🔄 Iniciando processo de recuperação de Provas e Questões...');
|
||||||
|
|
||||||
|
// Tentar ler do backup original
|
||||||
|
let backupPath = '/app/backup_supabase_2026-04-19.json';
|
||||||
|
if (!fs.existsSync(backupPath)) {
|
||||||
|
backupPath = path.join(process.cwd(), 'backup_supabase_2026-04-19.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(backupPath)) {
|
||||||
|
throw new Error(`Arquivo de backup não encontrado em: ${backupPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Lendo dados de backup em: ${backupPath}`);
|
||||||
|
const rawData = fs.readFileSync(backupPath, 'utf8');
|
||||||
|
const data = JSON.parse(rawData);
|
||||||
|
|
||||||
|
if (!data.exams || !Array.isArray(data.exams)) {
|
||||||
|
throw new Error('Chave "exams" não encontrada ou não é um array no backup.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Encontradas ${data.exams.length} provas no arquivo de backup.`);
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// Habilitar a gravação das provas no Postgres
|
||||||
|
let countProvasInseridas = 0;
|
||||||
|
let countQuestoesInseridas = 0;
|
||||||
|
|
||||||
|
for (const e of data.exams) {
|
||||||
|
if (!e.id || !e.title) continue;
|
||||||
|
|
||||||
|
// Inserir Prova
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO provas (id, turma_id, disciplina_id, periodo_id, titulo, duracao_minutos, status, permitir_refacao, is_deleted, evaluation_type, max_score)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
turma_id = COALESCE(provas.turma_id, EXCLUDED.turma_id),
|
||||||
|
disciplina_id = COALESCE(provas.disciplina_id, EXCLUDED.disciplina_id),
|
||||||
|
periodo_id = COALESCE(provas.periodo_id, EXCLUDED.periodo_id),
|
||||||
|
titulo = EXCLUDED.titulo,
|
||||||
|
duracao_minutos = EXCLUDED.duracao_minutos,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
permitir_refacao = EXCLUDED.permitir_refacao,
|
||||||
|
evaluation_type = EXCLUDED.evaluation_type,
|
||||||
|
max_score = EXCLUDED.max_score`,
|
||||||
|
[
|
||||||
|
e.id,
|
||||||
|
e.classId || e.turma_id || null,
|
||||||
|
e.subjectId || e.disciplina_id || null,
|
||||||
|
e.periodId || e.periodo_id || null,
|
||||||
|
e.title || e.titulo,
|
||||||
|
e.durationMinutes || e.duracao_minutos || 60,
|
||||||
|
e.status || 'draft',
|
||||||
|
e.allowRetake || e.permitir_refacao || false,
|
||||||
|
e.isDeleted || e.is_deleted || false,
|
||||||
|
e.evaluationType || e.evaluation_type || 'exam',
|
||||||
|
e.maxScore !== undefined ? Number(e.maxScore) : (e.max_score !== undefined ? Number(e.max_score) : 10.0)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
countProvasInseridas++;
|
||||||
|
|
||||||
|
// Inserir Questões da Prova
|
||||||
|
if (e.questions && Array.isArray(e.questions)) {
|
||||||
|
let ordem = 0;
|
||||||
|
for (const q of e.questions) {
|
||||||
|
if (!q.id || !q.text) continue;
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO questoes_provas (id, prova_id, texto, opcoes, indice_correto, ordem, imagem_url)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
texto = EXCLUDED.texto,
|
||||||
|
opcoes = EXCLUDED.opcoes,
|
||||||
|
indice_correto = EXCLUDED.indice_correto,
|
||||||
|
ordem = EXCLUDED.ordem,
|
||||||
|
imagem_url = COALESCE(questoes_provas.imagem_url, EXCLUDED.imagem_url)`,
|
||||||
|
[
|
||||||
|
q.id,
|
||||||
|
e.id,
|
||||||
|
q.text,
|
||||||
|
JSON.stringify(q.options || []),
|
||||||
|
q.correctOptionIndex !== undefined ? q.correctOptionIndex : (q.indice_correto ?? 0),
|
||||||
|
q.order !== undefined ? q.order : (q.ordem ?? ordem++),
|
||||||
|
q.imageUrl || q.imagem_url || null
|
||||||
|
]
|
||||||
|
);
|
||||||
|
countQuestoesInseridas++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Restaurando chaves no school_data JSON da tabela school_data no Postgres...`);
|
||||||
|
const { rows } = await client.query('SELECT data FROM school_data WHERE id = 1');
|
||||||
|
if (rows[0]?.data) {
|
||||||
|
const currentJson = rows[0].data;
|
||||||
|
// Atualizar a chave exams no JSON
|
||||||
|
currentJson.exams = data.exams;
|
||||||
|
await client.query('UPDATE school_data SET data = $1, updated_at = NOW() WHERE id = 1', [JSON.stringify(currentJson)]);
|
||||||
|
console.log('Chave exams atualizada no JSON school_data da tabela.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
console.log(`✅ Sucesso! Foram restauradas ${countProvasInseridas} provas e ${countQuestoesInseridas} questões no PostgreSQL.`);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
console.error('❌ Erro na recuperação:', err);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recover();
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import pg from 'pg';
|
||||||
|
|
||||||
|
const { Pool } = pg;
|
||||||
|
|
||||||
|
const connectionString = "postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager";
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString,
|
||||||
|
ssl: false
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log("Conectando ao banco de dados na VPS...");
|
||||||
|
const client = await pool.connect();
|
||||||
|
console.log("Conexão bem-sucedida!");
|
||||||
|
|
||||||
|
console.log("Executando SELECT na tabela alunos...");
|
||||||
|
const res = await client.query("SELECT id, nome, email, telefone, numero_matricula FROM alunos LIMIT 5;");
|
||||||
|
|
||||||
|
console.log(`Retornados ${res.rows.length} alunos:`);
|
||||||
|
console.log(JSON.stringify(res.rows, null, 2));
|
||||||
|
|
||||||
|
client.release();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro na conexão ou na query:", err.message);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -1279,7 +1279,13 @@ app.post('/api/ai/generate-questions', async (req, res) => {
|
||||||
|
|
||||||
if (provider === 'gemini') {
|
if (provider === 'gemini') {
|
||||||
apiKey = aiConfig.geminiKey;
|
apiKey = aiConfig.geminiKey;
|
||||||
modelName = aiConfig.geminiModel || 'gemini-1.5-flash';
|
modelName = aiConfig.geminiModel || 'gemini-2.0-flash';
|
||||||
|
// Validar modelos Gemini conhecidos como inexistentes/descontinuados
|
||||||
|
const invalidGeminiModels = ['gemini-1.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-flash-lite-preview'];
|
||||||
|
if (invalidGeminiModels.includes(modelName)) {
|
||||||
|
console.warn(`[IA] Modelo "${modelName}" descontinuado/inexistente. Usando gemini-2.0-flash como fallback.`);
|
||||||
|
modelName = 'gemini-2.0-flash';
|
||||||
|
}
|
||||||
} else if (provider === 'openai') {
|
} else if (provider === 'openai') {
|
||||||
apiKey = aiConfig.openaiKey;
|
apiKey = aiConfig.openaiKey;
|
||||||
modelName = aiConfig.openaiModel || 'gpt-4o-mini';
|
modelName = aiConfig.openaiModel || 'gpt-4o-mini';
|
||||||
|
|
@ -1549,30 +1555,30 @@ Apenas retorne o JSON estruturado contendo a lista de questões no seguinte form
|
||||||
console.warn('[Gemini] Erro na primeira tentativa:', err.message);
|
console.warn('[Gemini] Erro na primeira tentativa:', err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tentativa 2 (Fallback): Se a tentativa 1 deu 404 (modelo inválido/descontinuado) ou outro erro crítico, tentamos gemini-1.5-flash com Schema
|
// Tentativa 2 (Fallback): Se a tentativa 1 deu 404 (modelo inválido/descontinuado) ou outro erro crítico, tentamos gemini-2.0-flash com Schema
|
||||||
if (!resObj || !resObj.response.ok) {
|
if (!resObj || !resObj.response.ok) {
|
||||||
try {
|
try {
|
||||||
modelTentado = 'gemini-1.5-flash';
|
modelTentado = 'gemini-2.0-flash';
|
||||||
console.log(`[Gemini] Tentando fallback para gemini-1.5-flash com Schema...`);
|
console.log(`[Gemini] Tentando fallback para gemini-2.0-flash com Schema...`);
|
||||||
resObj = await tentarChamadaGemini(modelTentado, true);
|
resObj = await tentarChamadaGemini(modelTentado, true);
|
||||||
|
|
||||||
if (!resObj.response.ok && resObj.response.status === 400) {
|
if (!resObj.response.ok && resObj.response.status === 400) {
|
||||||
const errText = await resObj.response.clone().text();
|
const errText = await resObj.response.clone().text();
|
||||||
if (errText.includes('responseMimeType') || errText.includes('responseSchema')) {
|
if (errText.includes('responseMimeType') || errText.includes('responseSchema')) {
|
||||||
console.warn('[Gemini] gemini-1.5-flash rejeitou Schema. Re-tentando sem Schema...');
|
console.warn('[Gemini] gemini-2.0-flash rejeitou Schema. Re-tentando sem Schema...');
|
||||||
resObj = await tentarChamadaGemini(modelTentado, false);
|
resObj = await tentarChamadaGemini(modelTentado, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[Gemini] Erro no fallback gemini-1.5-flash:', err.message);
|
console.warn('[Gemini] Erro no fallback gemini-2.0-flash:', err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tentativa 3 (Último recurso): Se ainda falhar, tentamos gemini-1.5-flash sem Schema diretamente
|
// Tentativa 3 (Último recurso): Se ainda falhar, tentamos gemini-2.0-flash sem Schema diretamente
|
||||||
if (!resObj || !resObj.response.ok) {
|
if (!resObj || !resObj.response.ok) {
|
||||||
try {
|
try {
|
||||||
modelTentado = 'gemini-1.5-flash';
|
modelTentado = 'gemini-2.0-flash';
|
||||||
console.log(`[Gemini] Tentando fallback final para gemini-1.5-flash sem Schema...`);
|
console.log(`[Gemini] Tentando fallback final para gemini-2.0-flash sem Schema...`);
|
||||||
resObj = await tentarChamadaGemini(modelTentado, false);
|
resObj = await tentarChamadaGemini(modelTentado, false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[Gemini] Erro no fallback final:', err.message);
|
console.warn('[Gemini] Erro no fallback final:', err.message);
|
||||||
|
|
@ -1880,7 +1886,7 @@ app.post('/api/ai/list-models', async (req, res) => {
|
||||||
}));
|
}));
|
||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
models = [
|
models = [
|
||||||
{ id: 'gemini-1.5-flash', name: 'gemini-1.5-flash (Mais Rápido & Gratuito)' },
|
{ id: 'gemini-2.0-flash', name: 'gemini-2.0-flash (Mais Rápido & Gratuito)' },
|
||||||
{ id: 'gemini-1.5-pro', name: 'gemini-1.5-pro (Multimodal)' },
|
{ id: 'gemini-1.5-pro', name: 'gemini-1.5-pro (Multimodal)' },
|
||||||
{ id: 'gemini-2.0-flash-exp', name: 'gemini-2.0-flash-exp (Experimental)' },
|
{ id: 'gemini-2.0-flash-exp', name: 'gemini-2.0-flash-exp (Experimental)' },
|
||||||
{ id: 'gemini-2.5-flash', name: 'gemini-2.5-flash (Se ativo na sua conta)' }
|
{ id: 'gemini-2.5-flash', name: 'gemini-2.5-flash (Se ativo na sua conta)' }
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export async function verificarEInicializarTabelaIA() {
|
||||||
openai_key TEXT,
|
openai_key TEXT,
|
||||||
claude_key TEXT,
|
claude_key TEXT,
|
||||||
openrouter_key TEXT,
|
openrouter_key TEXT,
|
||||||
gemini_model VARCHAR(100) DEFAULT 'gemini-1.5-flash',
|
gemini_model VARCHAR(100) DEFAULT 'gemini-2.0-flash',
|
||||||
openai_model VARCHAR(100) DEFAULT 'gpt-4o-mini',
|
openai_model VARCHAR(100) DEFAULT 'gpt-4o-mini',
|
||||||
claude_model VARCHAR(100) DEFAULT 'claude-3-5-sonnet-20241022',
|
claude_model VARCHAR(100) DEFAULT 'claude-3-5-sonnet-20241022',
|
||||||
openrouter_model VARCHAR(100) DEFAULT 'google/gemini-2.5-flash',
|
openrouter_model VARCHAR(100) DEFAULT 'google/gemini-2.5-flash',
|
||||||
|
|
@ -90,7 +90,7 @@ export async function getSchoolData() {
|
||||||
openaiKey,
|
openaiKey,
|
||||||
claudeKey,
|
claudeKey,
|
||||||
openrouterKey,
|
openrouterKey,
|
||||||
geminiModel: dbConfig.gemini_model || legacyAiConfig.geminiModel || 'gemini-1.5-flash',
|
geminiModel: dbConfig.gemini_model || legacyAiConfig.geminiModel || 'gemini-2.0-flash',
|
||||||
openaiModel: dbConfig.openai_model || legacyAiConfig.openaiModel || 'gpt-4o-mini',
|
openaiModel: dbConfig.openai_model || legacyAiConfig.openaiModel || 'gpt-4o-mini',
|
||||||
claudeModel: dbConfig.claude_model || legacyAiConfig.claudeModel || 'claude-3-5-sonnet-20241022',
|
claudeModel: dbConfig.claude_model || legacyAiConfig.claudeModel || 'claude-3-5-sonnet-20241022',
|
||||||
openrouterModel: dbConfig.openrouter_model || legacyAiConfig.openrouterModel || 'google/gemini-2.5-flash',
|
openrouterModel: dbConfig.openrouter_model || legacyAiConfig.openrouterModel || 'google/gemini-2.5-flash',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import pg from 'pg';
|
||||||
|
|
||||||
|
const connectionString = 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager';
|
||||||
|
const pool = new pg.Pool({ connectionString });
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const { rows: schoolDataRows } = await pool.query('SELECT length(data::text) as len, (data->\'exams\') is not null as has_exams FROM school_data WHERE id = 1');
|
||||||
|
console.log('School Data:', schoolDataRows);
|
||||||
|
|
||||||
|
if (schoolDataRows[0]?.has_exams) {
|
||||||
|
const { rows: examCount } = await pool.query('SELECT jsonb_array_length(data->\'exams\') as count FROM school_data WHERE id = 1');
|
||||||
|
console.log('Quantidade de provas no school_data JSON:', examCount[0]?.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rows: countProvas } = await pool.query('SELECT count(*) as count FROM provas');
|
||||||
|
console.log('Quantidade de provas na tabela PROVAS (SQL):', countProvas[0]?.count);
|
||||||
|
|
||||||
|
const { rows: countQuestoes } = await pool.query('SELECT count(*) as count FROM questoes_provas');
|
||||||
|
console.log('Quantidade de questões na tabela QUESTAO_PROVAS (SQL):', countQuestoes[0]?.count);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao conectar ao banco:', err);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
Loading…
Reference in New Issue