Merge origin/main into Servidor
# Conflicts: # Dockerfile # client/src/pages/DevCenter.tsx # client/src/pages/DevelopmentModule.tsx # server/routes.ts
This commit is contained in:
commit
f3322ab075
|
|
@ -0,0 +1,20 @@
|
|||
PGPASSWORD=afefb610090de1646944b537f3054715ad5755665c90036c2bcd258c9ab52dd8
|
||||
SESSION_SECRET=092a26dcbebd6f7f5e7b9e020bdc4d4e966286c9780ef72e3426f437eb8d608b
|
||||
SSO_SECRET=fbb87d1c5a14a6623d4e3dcc2b679bd68c46e59afb48dd6abd42b95cac945856
|
||||
LITELLM_API_KEY=e396b827cbeb5e77d85cce4eb9da0521114f57a8e799f7aa8febad08d776c07e
|
||||
WEBUI_SECRET_KEY=76c7fc044f9e8d6e5eb60b67c82832b67e84739da4827ce1fa7c101ae1a055df
|
||||
DOMAIN=suite.onboardbi.com.br
|
||||
OLLAMA_BASE_URL=http://ollama-ia1upsekrad96at5hq97e4qa:11434
|
||||
|
||||
LLMFIT_BASE_URL=http://llmfit:8000
|
||||
LLMFIT_API_KEY=key-arcadia
|
||||
|
||||
# ── Superset BI ───────────────────────────────────────────────
|
||||
SUPERSET_SECRET_KEY=421274b5ba360778a5398d528116a45ea962d1197e3dfc99f36a372ff1025a63
|
||||
SUPERSET_ADMIN_USERNAME=admin
|
||||
SUPERSET_ADMIN_PASSWORD=arcadia1337
|
||||
SUPERSET_ADMIN_EMAIL=engenharia@ometas.com.br
|
||||
COMPOSE_PROFILES=bi
|
||||
|
||||
SUPERSET_HOST=superset
|
||||
SUPERSET_PORT=8088
|
||||
152
.env.example
152
.env.example
|
|
@ -1,70 +1,110 @@
|
|||
# ==============================================
|
||||
# Arcádia Suite - Variáveis de Ambiente
|
||||
# ==============================================
|
||||
# Copie este arquivo para .env e preencha os valores
|
||||
# ─── Arcádia Suite — Variáveis de Ambiente ────────────────────────────────────
|
||||
# Copie para .env e preencha os valores.
|
||||
# NUNCA commite o arquivo .env real.
|
||||
|
||||
# ---- Banco de Dados PostgreSQL ----
|
||||
DATABASE_URL=postgresql://arcadia:SuaSenhaSegura@db:5432/arcadia
|
||||
PGHOST=db
|
||||
# ── Banco de dados ────────────────────────────────────────────────────────────
|
||||
DATABASE_URL=postgresql://arcadia:arcadia123@localhost:5432/arcadia
|
||||
PGHOST=localhost
|
||||
PGPORT=5432
|
||||
PGUSER=arcadia
|
||||
PGPASSWORD=SuaSenhaSegura
|
||||
PGPASSWORD=arcadia123
|
||||
PGDATABASE=arcadia
|
||||
|
||||
# ---- Servidor ----
|
||||
NODE_ENV=production
|
||||
# ── Aplicação ─────────────────────────────────────────────────────────────────
|
||||
NODE_ENV=development
|
||||
PORT=5000
|
||||
SESSION_SECRET=gere-uma-chave-secreta-aleatoria-aqui
|
||||
SESSION_SECRET=troque-por-string-aleatoria-segura-em-producao
|
||||
SSO_SECRET=arcadia-sso-secret-2024-plus-integration-key-secure
|
||||
|
||||
# ---- SSO / Multi-tenant ----
|
||||
SSO_SECRET=gere-outra-chave-secreta-aqui
|
||||
# ── Docker Mode (desativa spawn de processos filhos) ─────────────────────────
|
||||
# Em produção com Docker, defina como "true"
|
||||
DOCKER_MODE=false
|
||||
|
||||
# ---- Senha do Gerente (Retail) ----
|
||||
MANAGER_PASSWORD=sua-senha-de-gerente
|
||||
# ── URLs dos microserviços Python ─────────────────────────────────────────────
|
||||
# Em Docker: use nome do serviço (ex: http://contabil:8003)
|
||||
# Em desenvolvimento local: use localhost
|
||||
CONTABIL_PYTHON_URL=http://localhost:8003
|
||||
BI_PYTHON_URL=http://localhost:8004
|
||||
AUTOMATION_PYTHON_URL=http://localhost:8005
|
||||
FISCO_PYTHON_URL=http://localhost:8002
|
||||
PYTHON_SERVICE_URL=http://localhost:8001
|
||||
|
||||
# ---- OpenAI (opcional - para funcionalidades de IA) ----
|
||||
OPENAI_API_KEY=sk-...
|
||||
AI_INTEGRATIONS_OPENAI_API_KEY=sk-...
|
||||
AI_INTEGRATIONS_OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
# ── IA — OpenAI ───────────────────────────────────────────────────────────────
|
||||
# Deixe vazio se usar apenas Ollama (soberania total)
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# ---- ERPNext (opcional - integração ERP) ----
|
||||
ERPNEXT_URL=https://seu-erpnext.com
|
||||
# ── IA — LiteLLM (gateway unificado — ÚNICA porta de entrada para LLMs) ──────
|
||||
# Em Docker: http://litellm:4000 | Em dev local: http://localhost:4000
|
||||
LITELLM_BASE_URL=http://localhost:4000
|
||||
LITELLM_API_KEY=arcadia-internal
|
||||
|
||||
# ── IA — Manus Agent (aponta para LiteLLM como gateway) ──────────────────────
|
||||
# Em Docker: AI_INTEGRATIONS_OPENAI_BASE_URL=http://litellm:4000/v1
|
||||
# Em dev local: http://localhost:4000/v1
|
||||
AI_INTEGRATIONS_OPENAI_BASE_URL=http://localhost:4000/v1
|
||||
AI_INTEGRATIONS_OPENAI_API_KEY=arcadia-internal
|
||||
|
||||
# ── IA — Ollama (LLMs locais — soberania total) ───────────────────────────────
|
||||
# Se Ollama está no host (fora do Docker): OLLAMA_BASE_URL=http://localhost:11434
|
||||
# Se Ollama está em outro servidor: OLLAMA_BASE_URL=http://IP_DO_SERVIDOR:11434
|
||||
OLLAMA_BASE_URL=http://localhost:11434
|
||||
|
||||
# ── IA — LLMFit (modelos fine-tuned locais — habilitar quando disponível) ─────
|
||||
# LLMFit turbocharge: modelos treinados com dados do seu negócio
|
||||
# Deixe vazio para desabilitar (LiteLLM cai para Ollama automaticamente)
|
||||
LLMFIT_BASE_URL=
|
||||
|
||||
# ── IA — Providers externos (opt-in — soberania: dados não saem sem configurar)
|
||||
# Deixe vazio para operação 100% soberana (apenas Ollama + LLMFit)
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
GROQ_API_KEY=
|
||||
|
||||
# ── Open WebUI ────────────────────────────────────────────────────────────────
|
||||
WEBUI_SECRET_KEY=troque-por-string-aleatoria-segura
|
||||
|
||||
# ── Arcádia Plus (Laravel/Fiscal) ─────────────────────────────────────────────
|
||||
PLUS_URL=http://localhost:8080
|
||||
PLUS_PORT=8080
|
||||
PLUS_API_TOKEN=
|
||||
|
||||
# ── Superset (BI avançado — Arcádia Insights) ────────────────────────────────
|
||||
SUPERSET_HOST=superset
|
||||
SUPERSET_PORT=8088
|
||||
SUPERSET_SECRET_KEY=troque-por-string-aleatoria-segura # openssl rand -hex 32
|
||||
SUPERSET_ADMIN_USER=admin
|
||||
SUPERSET_ADMIN_EMAIL=admin@arcadia.app
|
||||
SUPERSET_ADMIN_PASSWORD=troque-senha-forte-em-prod
|
||||
# URL do banco Arcádia para o Superset acessar os dados (idealmente read-only)
|
||||
ARCADIA_DATABASE_URL=postgresql://arcadia:arcadia123@db:5432/arcadia
|
||||
|
||||
# ── Redis ─────────────────────────────────────────────────────────────────────
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# ── Domínio (produção) ────────────────────────────────────────────────────────
|
||||
DOMAIN=seudominio.com.br
|
||||
|
||||
# ── ERPNext ────────────────────────────────────────────────────────────────────
|
||||
# Modo 1 — ERPNext em container Docker (perfil erpnext)
|
||||
# Sobe com: docker compose --profile erpnext up
|
||||
# Proxy ativado automaticamente em DOCKER_MODE=true
|
||||
ERPNEXT_PROXY_ENABLED=false # true para forçar proxy mesmo fora do Docker
|
||||
ERPNEXT_CONTAINER_HOST=erpnext # nome do serviço no docker-compose
|
||||
ERPNEXT_CONTAINER_PORT=8080
|
||||
ERPNEXT_DB_ROOT_PASSWORD=erpnext-root-2026 # senha root do MariaDB
|
||||
ERPNEXT_DB_PASSWORD=frappe2026 # senha do usuário frappe
|
||||
ERPNEXT_ADMIN_PASSWORD=admin2026 # senha do Administrator no site
|
||||
|
||||
# Modo 2 — ERPNext externo (instância existente)
|
||||
# Em Docker: ERPNEXT_URL=http://erpnext:8080 (aponta pro container)
|
||||
# Externo: ERPNEXT_URL=https://meuerpnext.com
|
||||
ERPNEXT_URL=
|
||||
ERPNEXT_API_KEY=
|
||||
ERPNEXT_API_SECRET=
|
||||
|
||||
# ---- GitHub (opcional - integração DevCenter) ----
|
||||
GITHUB_TOKEN=ghp_...
|
||||
GITHUB_OWNER=seu-usuario
|
||||
GITHUB_REPO=seu-repositorio
|
||||
GITHUB_DEFAULT_BRANCH=main
|
||||
# GitHub (para sync de código)
|
||||
GITHUB_TOKEN=
|
||||
GITHUB_REPO=
|
||||
|
||||
# ---- Arcádia Plus - ERP Laravel (opcional) ----
|
||||
PLUS_URL=http://localhost:8080
|
||||
PLUS_API_TOKEN=
|
||||
|
||||
# ---- Serviços Python (opcional) ----
|
||||
PYTHON_SERVICE_URL=http://localhost:8001
|
||||
FISCO_PYTHON_URL=http://localhost:8002
|
||||
FISCO_PORT=8002
|
||||
CONTABIL_PYTHON_URL=http://localhost:8003
|
||||
CONTABIL_PORT=8003
|
||||
PEOPLE_PYTHON_URL=http://localhost:8004
|
||||
PEOPLE_PORT=8004
|
||||
|
||||
# ---- Protocolos MCP/A2A (opcional) ----
|
||||
PROTOCOL_API_KEYS=sua-api-key-aqui
|
||||
|
||||
# ---- Apache Superset — BI Visual (opcional, ativar com --profile bi) ----
|
||||
# Gerar chave: openssl rand -hex 32
|
||||
SUPERSET_SECRET_KEY=gere-com-openssl-rand-hex-32
|
||||
SUPERSET_ADMIN_USER=admin
|
||||
SUPERSET_ADMIN_EMAIL=admin@arcadia.app
|
||||
SUPERSET_ADMIN_PASSWORD=troque-em-producao
|
||||
# Domínio público do Superset (apenas prod/Traefik)
|
||||
SUPERSET_DOMAIN=bi.seudominio.com.br
|
||||
|
||||
# Remover do .env se existirem (não são mais necessários):
|
||||
# METABASE_HOST
|
||||
# METABASE_PORT
|
||||
# METASET_ADMIN_EMAIL
|
||||
# METASET_ADMIN_PASSWORD
|
||||
# WhatsApp (Baileys — sessões salvas no banco)
|
||||
# Nenhuma variável adicional necessária — configurado via interface
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# Replit specific
|
||||
.cache/
|
||||
|
|
@ -63,18 +60,4 @@ attached_assets/
|
|||
|
||||
# Package lock (regenerated on install)
|
||||
package-lock.json
|
||||
|
||||
# Documentation files (except essential)
|
||||
MAPA_*.md
|
||||
AUDITORIA_*.md
|
||||
AVALIACAO_*.md
|
||||
DOCUMENTATION.md
|
||||
HOSPEDAGEM.md
|
||||
PLANO_*.md
|
||||
RELATORIO_*.md
|
||||
REQUISITOS_*.md
|
||||
*.pdf
|
||||
*.txt
|
||||
!README.md
|
||||
!replit.md
|
||||
.aider*
|
||||
.claude/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,475 @@
|
|||
# Fase 4: OpenClaw Embutido — Plano de Implementação
|
||||
|
||||
**Fase:** 4
|
||||
**Data Início:** 2026-03-26
|
||||
**Prazo:** 2026-04-08 (Semanas 7-8)
|
||||
**Status:** 🔄 Sprint 3 Pendente (Sprint 1 e 2 concluídos)
|
||||
|
||||
---
|
||||
|
||||
## 1. OBJETIVO DA FASE
|
||||
|
||||
Implementar **detecção proativa de padrões** e **sugestão emergente de skills**, transformando comportamentos repetitivos do usuário em automações reutilizáveis através de um widget conversacional flutuante.
|
||||
|
||||
### Entregável Final
|
||||
✅ Skills emergentes funcionando end-to-end:
|
||||
- Usuário executa ação repetida → PatternDetector detecta → OpenClawWidget sugere → Blackboard gera DRAFT → Dev Center aprova → Skill disponível
|
||||
|
||||
---
|
||||
|
||||
## 2. ARQUITETURA
|
||||
|
||||
```
|
||||
FLUXO OPENCCLAW:
|
||||
|
||||
1. USER INTERACTION
|
||||
└─ Usuário usa Arcádia
|
||||
└─ Learning API registra evento (já existe)
|
||||
└─ Neo4j armazena interação
|
||||
|
||||
2. PATTERN DETECTION (NOVO)
|
||||
└─ PatternDetector analisa Learning API
|
||||
└─ Identifica: 3+ ocorrências em 30 dias
|
||||
└─ Calcula confiança (threshold 0.8+)
|
||||
└─ Dispara evento no Socket.IO
|
||||
|
||||
3. WIDGET SUGGESTION (NOVO)
|
||||
└─ OpenClawWidget fica "ouvindo"
|
||||
└─ Recebe evento de padrão detectado
|
||||
└─ Exibe notificação flutuante
|
||||
└─ Modal SkillSuggestion mostra preview
|
||||
|
||||
4. SKILL GENERATION (NOVO)
|
||||
└─ Usuário confirma
|
||||
└─ OpenClawEngine chama Blackboard
|
||||
└─ Blackboard gera código (DRAFT)
|
||||
└─ Armazena em database
|
||||
|
||||
5. APPROVAL FLOW (EXISTENTE - Dev Center)
|
||||
└─ Skill DRAFT aparece em Dev Center
|
||||
└─ Admin/Lead revisa e aprova
|
||||
└─ Publica no tenant
|
||||
└─ Skill fica disponível para reutilização
|
||||
```
|
||||
|
||||
### Componentes para Implementar
|
||||
|
||||
```
|
||||
BACKEND:
|
||||
server/modules/openclaw/
|
||||
├── PatternDetector.ts ← [NOVO] Detecta padrões
|
||||
├── SkillEmergence.ts ← [NOVO] Coordena criação
|
||||
├── OpenClawEngine.ts ← [NOVO] Lógica central
|
||||
├── config/
|
||||
│ └── arcadia.config.yaml ← [NOVO] Configuração
|
||||
|
||||
FRONTEND:
|
||||
client/src/components/openclaw/
|
||||
├── OpenClawWidget.tsx ← [NOVO] Widget flutuante
|
||||
├── PatternDetector.ts ← [NOVO] Hook de detecção
|
||||
├── SkillSuggestion.tsx ← [NOVO] Modal de sugestão
|
||||
└── useAgentEmergence.ts ← [NOVO] Lógica de emergência
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. DEPENDÊNCIAS VERIFICADAS
|
||||
|
||||
| Componente | Status | Localização |
|
||||
|-----------|--------|-----------|
|
||||
| **Skills Engine** | ✅ Pronto | `server/skills/engine.ts` |
|
||||
| **Blackboard** | ✅ Pronto | `server/blackboard/service.ts` |
|
||||
| **Dev Center** | ✅ Pronto | `client/src/pages/DevCenter.tsx` |
|
||||
| **Learning API** | ✅ Existe | `/api/learning/*` |
|
||||
| **Neo4j (KG)** | ✅ Running | Docker |
|
||||
| **Socket.IO** | ✅ Configurado | `server/socket-io.ts` |
|
||||
| **PostgreSQL** | ✅ Rodando | Docker |
|
||||
| **Scientist (MiroFlow)** | ✅ Existe | `/api/manus/scientist/*` |
|
||||
|
||||
**Conclusão:** Todas as dependências estão prontas ✅
|
||||
|
||||
---
|
||||
|
||||
## 4. TASKS DETALHADAS
|
||||
|
||||
### SPRINT 1 (26-27/03): PatternDetector Backend — ✅ COMPLETO
|
||||
|
||||
#### Task 1.1: Criar PatternDetector.ts ✅
|
||||
**Arquivo:** `server/modules/openclaw/PatternDetector.ts`
|
||||
**Objetivo:** Analisar eventos de usuário e detectar padrões
|
||||
|
||||
```typescript
|
||||
// Pseudocódigo da estrutura
|
||||
export class PatternDetector {
|
||||
// Config
|
||||
min_occurrences: 3;
|
||||
time_window_days: 30;
|
||||
confidence_threshold: 0.8;
|
||||
|
||||
// Métodos
|
||||
detectPattern(userId: string, action: string): Pattern | null
|
||||
calculateConfidence(interactions: Interaction[]): number
|
||||
storePattern(pattern: Pattern): Promise<void>
|
||||
emitEvent(pattern: Pattern): void
|
||||
}
|
||||
```
|
||||
|
||||
**Entrada:** Eventos do Learning API (`/api/learning/interactions`)
|
||||
**Saída:** Pattern objects armazenados em Neo4j + Socket.IO event
|
||||
**Dependência:** Learning API funcionando ✅
|
||||
|
||||
#### Task 1.2: Criar OpenClawEngine.ts ✅
|
||||
**Arquivo:** `server/modules/openclaw/OpenClawEngine.ts`
|
||||
**Objetivo:** Orquestração central de OpenClaw
|
||||
|
||||
```typescript
|
||||
export class OpenClawEngine {
|
||||
patternDetector: PatternDetector;
|
||||
skillEmergence: SkillEmergence;
|
||||
|
||||
// Métodos
|
||||
async suggestSkill(pattern: Pattern): Promise<SkillSuggestion>
|
||||
async onUserConfirmation(suggestion: SkillSuggestion): Promise<Skill>
|
||||
async checkPatternsPeriodicly(): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
**Entrada:** Padrões detectados
|
||||
**Saída:** Sugestões + Skills gerados
|
||||
**Evento:** Socket.IO broadcast `/openclaw/pattern-detected`
|
||||
|
||||
#### Task 1.3: Criar SkillEmergence.ts ✅
|
||||
**Arquivo:** `server/modules/openclaw/SkillEmergence.ts`
|
||||
**Objetivo:** Integração com Blackboard para codegen
|
||||
|
||||
```typescript
|
||||
export class SkillEmergence {
|
||||
async generateSkillDraft(pattern: Pattern): Promise<SkillDraft> {
|
||||
// 1. Montar prompt baseado no padrão
|
||||
// 2. Chamar POST /api/blackboard/generate-skill
|
||||
// 3. Receber código gerado
|
||||
// 4. Salvar como DRAFT no database
|
||||
// 5. Notificar Dev Center
|
||||
// 6. Retornar skill DRAFT
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Integração:** POST `/api/blackboard/generate-skill`
|
||||
**Database:** skills table (com status: 'draft')
|
||||
|
||||
#### Task 1.4: Criar rotas OpenClaw ✅
|
||||
**Arquivo:** `server/modules/openclaw/routes.ts`
|
||||
**Rotas:**
|
||||
```
|
||||
POST /api/openclaw/detect-patterns ← Trigger manual de análise
|
||||
GET /api/openclaw/patterns ← Listar padrões detectados
|
||||
POST /api/openclaw/confirm-skill ← Usuário confirma sugestão
|
||||
GET /api/openclaw/suggestions ← Histórico de sugestões
|
||||
```
|
||||
|
||||
#### Task 1.5: Integrar em server/index.ts ✅
|
||||
**Arquivo:** `server/index.ts`
|
||||
**Ação:** Importar rotas OpenClaw e carregá-las
|
||||
|
||||
```typescript
|
||||
import openclawRoutes from "./modules/openclaw/routes";
|
||||
app.use("/api/openclaw", openclawRoutes);
|
||||
```
|
||||
|
||||
### SPRINT 2 (28-29/03): Frontend Widgets — ✅ COMPLETO (commit 5231166)
|
||||
|
||||
#### Task 2.1: Criar OpenClawWidget.tsx ✅
|
||||
**Arquivo:** `client/src/components/openclaw/OpenClawWidget.tsx`
|
||||
**Objetivo:** Widget flutuante que recebe notificações de padrões
|
||||
|
||||
```typescript
|
||||
export function OpenClawWidget() {
|
||||
// Escutar Socket.IO event: /openclaw/pattern-detected
|
||||
// Exibir notificação flutuante (canto inferior direito)
|
||||
// Mostrar padrão detectado
|
||||
// Botão "Ver Sugestão" abre modal
|
||||
// Botão "Ignorar" descarta
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4">
|
||||
{/* Widget flutuante */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Socket.IO:** Escuta evento `/openclaw/pattern-detected`
|
||||
**Trigger:** Modal SkillSuggestion
|
||||
|
||||
#### Task 2.2: Criar SkillSuggestion.tsx ✅
|
||||
**Arquivo:** `client/src/components/openclaw/SkillSuggestion.tsx`
|
||||
**Objetivo:** Modal com preview da skill sugerida
|
||||
|
||||
```typescript
|
||||
export function SkillSuggestion({ pattern, suggestion }) {
|
||||
// Modal mostra:
|
||||
// 1. "Detectamos um padrão na sua utilização:"
|
||||
// 2. Preview do padrão (ações repetidas)
|
||||
// 3. Preview da automação gerada
|
||||
// 4. Campo de nome (auto-preenchido)
|
||||
// 5. Descrição (auto-gerada)
|
||||
// 6. Botão "Criar Skill" ou "Cancelar"
|
||||
|
||||
const handleCreate = async () => {
|
||||
// POST /api/openclaw/confirm-skill
|
||||
// Esperar resposta (skill DRAFT)
|
||||
// Toast: "Skill criada! Vá para Dev Center para publicar"
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Integração:** Chama `POST /api/openclaw/confirm-skill`
|
||||
**Feedback:** Toast notification
|
||||
|
||||
#### Task 2.3: Hook useAgentEmergence.ts ✅
|
||||
**Arquivo:** `client/src/hooks/useAgentEmergence.ts`
|
||||
**Objetivo:** Lógica de emergência de agentes
|
||||
|
||||
```typescript
|
||||
export function useAgentEmergence(userId: string) {
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Socket.IO listener
|
||||
socket.on("openclaw:pattern-detected", (pattern) => {
|
||||
// Filtrar por tenant, user prefs
|
||||
// Atualizar suggestions state
|
||||
setSuggestions([...suggestions, pattern]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { suggestions, dismiss, confirm };
|
||||
}
|
||||
```
|
||||
|
||||
#### Task 2.4: Integrar Widget em layout principal ✅
|
||||
**Arquivo:** `client/src/App.tsx` ou `client/src/layouts/MainLayout.tsx`
|
||||
**Ação:** Adicionar OpenClawWidget ao layout persistente
|
||||
|
||||
```typescript
|
||||
import { OpenClawWidget } from "@/components/openclaw/OpenClawWidget";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
{/* Conteúdo existente */}
|
||||
<OpenClawWidget /> {/* ← Adicionar aqui */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### SPRINT 3 (30-31/03): Configuração e Integração — ⏳ PRÓXIMO
|
||||
|
||||
#### Task 3.1: Criar arcadia.config.yaml
|
||||
**Arquivo:** `server/modules/openclaw/config/arcadia.config.yaml`
|
||||
|
||||
```yaml
|
||||
openclaw:
|
||||
enabled: true
|
||||
|
||||
# Endpoints
|
||||
arcadia_api_url: http://localhost:3000/api
|
||||
arcadia_kg_url: http://localhost:7474
|
||||
blackboard_url: http://localhost:3000/api/blackboard
|
||||
|
||||
# Tenant awareness
|
||||
tenant_aware: true
|
||||
|
||||
# Detecção de padrões
|
||||
pattern_detection:
|
||||
enabled: true
|
||||
min_occurrences: 3
|
||||
time_window_days: 30
|
||||
confidence_threshold: 0.8
|
||||
scheduled_check_interval_minutes: 60
|
||||
|
||||
# Sugestões
|
||||
suggestions:
|
||||
enabled: true
|
||||
auto_suggest: false # Sempre pedir confirmação do usuário
|
||||
channels: ['widget', 'chat']
|
||||
priority_threshold: 0.85
|
||||
|
||||
# Skills emergentes
|
||||
emergence:
|
||||
create_as_draft: true # Sempre requer aprovação
|
||||
auto_publish: false
|
||||
notify_admins: true
|
||||
skill_prefix: "emergent_" # Prefixo para skills auto-geradas
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
enabled: true
|
||||
store_patterns: true
|
||||
store_in_kg: true
|
||||
audit_trail: true
|
||||
```
|
||||
|
||||
#### Task 3.2: Integrar Socket.IO events
|
||||
**Arquivo:** `server/socket-io.ts`
|
||||
**Ação:** Adicionar listeners e emitters para OpenClaw
|
||||
|
||||
```typescript
|
||||
socket.on("openclaw:confirm-skill", async (data) => {
|
||||
// Receber confirmação do usuário
|
||||
// Chamar SkillEmergence.generateSkillDraft()
|
||||
// Emitir "openclaw:skill-created"
|
||||
});
|
||||
|
||||
// Emitter no PatternDetector
|
||||
io.emit("openclaw:pattern-detected", pattern);
|
||||
```
|
||||
|
||||
#### Task 3.3: Database schema para OpenClaw
|
||||
**Arquivo:** `shared/schema.ts` (Drizzle ORM)
|
||||
**Adicionar tabelas:**
|
||||
|
||||
```typescript
|
||||
// Padrões detectados
|
||||
export const detectedPatterns = pgTable("detected_patterns", {
|
||||
id: serial().primaryKey(),
|
||||
tenant_id: uuid().notNull(),
|
||||
user_id: uuid().notNull(),
|
||||
action_type: varchar(255).notNull(),
|
||||
occurrences: integer().notNull(),
|
||||
confidence: numeric(3, 2).notNull(),
|
||||
time_window_days: integer().notNull(),
|
||||
created_at: timestamp().defaultNow(),
|
||||
metadata: jsonb(),
|
||||
});
|
||||
|
||||
// Sugestões geradas
|
||||
export const skillSuggestions = pgTable("skill_suggestions", {
|
||||
id: serial().primaryKey(),
|
||||
pattern_id: integer().references(() => detectedPatterns.id),
|
||||
skill_draft_id: uuid().references(() => skills.id),
|
||||
status: varchar(50).notNull(), // 'suggested', 'accepted', 'rejected'
|
||||
created_at: timestamp().defaultNow(),
|
||||
});
|
||||
```
|
||||
|
||||
#### Task 3.4: Migration SQL
|
||||
**Arquivo:** `migrations/0004_openclaw_tables.sql`
|
||||
|
||||
```sql
|
||||
CREATE TABLE detected_patterns (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id UUID NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
action_type VARCHAR(255) NOT NULL,
|
||||
occurrences INTEGER NOT NULL,
|
||||
confidence NUMERIC(3,2) NOT NULL,
|
||||
time_window_days INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata JSONB
|
||||
);
|
||||
|
||||
CREATE TABLE skill_suggestions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
pattern_id INTEGER REFERENCES detected_patterns(id),
|
||||
skill_draft_id UUID REFERENCES skills(id),
|
||||
status VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_detected_patterns_user_tenant
|
||||
ON detected_patterns(user_id, tenant_id);
|
||||
```
|
||||
|
||||
#### Task 3.5: Testes unitários
|
||||
**Arquivo:** `tests/openclaw.test.ts`
|
||||
- PatternDetector detecta padrão corretamente
|
||||
- OpenClawEngine orquestra fluxo correto
|
||||
- SkillEmergence integra com Blackboard
|
||||
- Socket.IO emite eventos corretamente
|
||||
|
||||
#### Task 3.6: Documentação
|
||||
**Arquivo:** `DOCUMENTATION.md` (adicionar seção)
|
||||
- Como OpenClaw funciona
|
||||
- Configuração por tenant
|
||||
- Troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## 5. TIMELINE
|
||||
|
||||
| Data | Sprint | Tasks | Status |
|
||||
|------|--------|-------|--------|
|
||||
| 26-27/03 | 1 | PatternDetector backend | ✅ Completo |
|
||||
| 26/03 | 2 | OpenClawWidget frontend | ✅ Completo (commit 5231166) |
|
||||
| 26/03 | 3 | Migration DB + testes | ⏳ Próximo |
|
||||
| 01-08/04 | Buffer | Refinamento, testes E2E | ⏳ Próximo |
|
||||
|
||||
**Nota:** Trabalho em `/opt/arcadia_merged/` (diretório canônico do projeto).
|
||||
|
||||
---
|
||||
|
||||
## 6. CRITÉRIOS DE SUCESSO
|
||||
|
||||
✅ **PatternDetector funciona:**
|
||||
- Detecta padrão após 3 ocorrências em 30 dias
|
||||
- Confiança calculada corretamente
|
||||
- Eventos emitidos para Socket.IO
|
||||
|
||||
✅ **OpenClawWidget funciona:**
|
||||
- Widget aparece quando padrão é detectado
|
||||
- Modal mostra sugestão corretamente
|
||||
- Usuário pode confirmar ou rejeitar
|
||||
|
||||
✅ **SkillEmergence funciona:**
|
||||
- Integra com Blackboard
|
||||
- Gera skill como DRAFT
|
||||
- Aparece em Dev Center para aprovação
|
||||
|
||||
✅ **E2E funciona:**
|
||||
- User faz ação 3x
|
||||
- Widget sugere
|
||||
- Approva
|
||||
- Skill publicada
|
||||
- Skill disponível para reutilização
|
||||
|
||||
---
|
||||
|
||||
## 7. RISCOS E MITIGAÇÕES
|
||||
|
||||
| Risco | Probabilidade | Mitigação |
|
||||
|-------|--------------|-----------|
|
||||
| Blackboard não retorna código correto | Média | Testar integração antes, ter fallback |
|
||||
| Padrões detectados incorretamente | Média | Validar threshold, testes no KG |
|
||||
| Socket.IO não emite em tempo real | Baixa | Já funciona em outras partes |
|
||||
| Performance com muitos eventos | Média | Usar índices em PG, cache em Redis |
|
||||
|
||||
---
|
||||
|
||||
## 8. BRANCH E COMMITS
|
||||
|
||||
**Branch:** `Servidor` (deploy branch, conforme feedback anterior)
|
||||
**Commit pattern:**
|
||||
```
|
||||
feat(openclaw): PatternDetector backend
|
||||
feat(openclaw): OpenClawWidget frontend
|
||||
feat(openclaw): Integração Blackboard + esquema DB
|
||||
docs(openclaw): Documentação e testes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. PRÓXIMAS AÇÕES
|
||||
|
||||
1. ✅ **26/03:** Sprint 1 concluído (PatternDetector, OpenClawEngine, SkillEmergence, routes, config)
|
||||
2. ✅ **26/03:** Sprint 2 concluído (OpenClawWidget, SkillSuggestion, useAgentEmergence, App.tsx — commit 5231166)
|
||||
3. ⏳ **Agora:** Sprint 3 — migration SQL + vitest + testes unitários em `/opt/arcadia_merged/`
|
||||
4. ⏳ **01-08/04:** Refinamento e testes E2E
|
||||
|
||||
---
|
||||
|
||||
*Fase 4: OpenClaw Embutido — Plano Executável*
|
||||
*Data: 2026-03-26*
|
||||
*Pronto para implementação ✅*
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# Quick Task 260326-kmz: Implementar Skill Fabric
|
||||
|
||||
**Description:** Implementar Skill Fabric — gerador e gestor de skills da Arcádia Suite
|
||||
**Date:** 2026-03-26
|
||||
**Mode:** quick
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Backend — Skill Fabric Module
|
||||
|
||||
**Files:**
|
||||
- `server/modules/skill-fabric/src/types/index.ts`
|
||||
- `server/modules/skill-fabric/src/core/skill/Skill.entity.ts`
|
||||
- `server/modules/skill-fabric/src/core/compiler/VisualCompiler.ts`
|
||||
- `server/modules/skill-fabric/src/core/compiler/CodeCompiler.ts`
|
||||
- `server/modules/skill-fabric/src/core/compiler/MarkdownCompiler.ts`
|
||||
- `server/modules/skill-fabric/src/core/validator/ValidationPipeline.ts`
|
||||
- `server/modules/skill-fabric/src/core/executor/SkillExecutor.ts`
|
||||
- `server/modules/skill-fabric/src/core/executor/Sandbox.ts`
|
||||
- `server/modules/skill-fabric/src/core/lifecycle/LifecycleManager.ts`
|
||||
- `server/modules/skill-fabric/src/api/routes/skills.routes.ts`
|
||||
- `server/modules/skill-fabric/src/api/controllers/skills.controller.ts`
|
||||
- `server/modules/skill-fabric/index.ts`
|
||||
|
||||
**Action:** Create the full backend Skill Fabric module with types, entity, compilers, validator pipeline, executor/sandbox, lifecycle manager, and API routes. The project uses TypeScript + Express + Drizzle ORM. Reuse existing `db` and `skills` schema from `../../shared/schema`. No new DB migration needed — extend existing skills table logic.
|
||||
|
||||
**Context:**
|
||||
- Existing `server/skills/routes.ts` has basic CRUD. The new module adds compilation, validation, execution, versioning, lifecycle.
|
||||
- DB import: `import { db } from "@/db"` or `import { db } from "../../../db"` — check what works
|
||||
- Existing schema: `skills`, `skillExecutions` from `../../shared/schema`
|
||||
- Do NOT use vm2 (needs install) — use Node.js built-in `vm` module for sandbox
|
||||
- Do NOT use ReactFlow/Monaco yet (frontend task) — backend only here
|
||||
|
||||
**Done:** Files created, module compiles without errors
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Frontend — Skill Fabric UI
|
||||
|
||||
**Files:**
|
||||
- `client/src/modules/skill-fabric/index.ts`
|
||||
- `client/src/modules/skill-fabric/types.ts`
|
||||
- `client/src/modules/skill-fabric/api.ts`
|
||||
- `client/src/modules/skill-fabric/code-ide/components/SkillCodeEditor.tsx`
|
||||
- `client/src/modules/skill-fabric/markdown-studio/components/MarkdownEditor.tsx`
|
||||
- `client/src/modules/skill-fabric/shared/components/SkillToolbar.tsx`
|
||||
- `client/src/modules/skill-fabric/shared/components/VersionSelector.tsx`
|
||||
- `client/src/modules/skill-fabric/shared/components/ValidationPanel.tsx`
|
||||
- `client/src/pages/SkillFabricPage.tsx`
|
||||
|
||||
**Action:** Create frontend Skill Fabric module. Use textarea-based code editor (Monaco not installed — don't add it). Use Tailwind CSS + shadcn/ui components matching the existing project style. Create a SkillFabricPage that lists skills, allows creating/editing (code + markdown modes), compiling, validating, and executing. Wire to the backend API routes from Task 1.
|
||||
|
||||
**Context:**
|
||||
- Check `client/src/components/` for existing UI patterns (shadcn components available)
|
||||
- Check `client/src/App.tsx` to see how routes are registered — add the new page there
|
||||
- The visual canvas (ReactFlow) is a stretch goal — skip it, implement code + markdown modes only
|
||||
- API base: `/api/skills` (existing) + `/api/skill-fabric` (new from Task 1)
|
||||
|
||||
**Done:** SkillFabricPage renders, skills can be listed and created
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Register Routes & Integration
|
||||
|
||||
**Files:**
|
||||
- `server/modules/loader.ts`
|
||||
- `client/src/App.tsx` (or router file)
|
||||
|
||||
**Action:** Register the new skill-fabric backend routes in `server/modules/loader.ts`. Add the SkillFabricPage route in the frontend router. Verify existing skills route still works.
|
||||
|
||||
**Done:** `/api/skill-fabric/*` routes registered, frontend page accessible
|
||||
8
.replit
8
.replit
|
|
@ -4,16 +4,12 @@ hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
|
|||
|
||||
[nix]
|
||||
channel = "stable-24_05"
|
||||
packages = ["glibcLocales", "libiconv", "libxcrypt", "openssl", "pkg-config", "unzip", "zlib", "zip"]
|
||||
packages = ["cairo", "cargo", "ffmpeg-full", "freetype", "ghostscript", "gitFull", "glibcLocales", "gobject-introspection", "gtk3", "libiconv", "libxcrypt", "mysql80", "openssl", "php82", "php82Packages.composer", "pkg-config", "playwright-driver", "qhull", "rustc", "tcl", "tk", "unzip", "xcodebuild", "zlib", "jdk21"]
|
||||
|
||||
[[ports]]
|
||||
localPort = 5000
|
||||
externalPort = 80
|
||||
|
||||
[[ports]]
|
||||
localPort = 5904
|
||||
externalPort = 6800
|
||||
|
||||
[[ports]]
|
||||
localPort = 8001
|
||||
externalPort = 3001
|
||||
|
|
@ -53,7 +49,7 @@ PORT = "5000"
|
|||
deploymentTarget = "autoscale"
|
||||
build = ["npm", "run", "build"]
|
||||
publicDir = "dist/public"
|
||||
run = ["npm", "run", "start"]
|
||||
run = ["node", "./dist/index.cjs"]
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,553 @@
|
|||
# Arcádia Suite - Documentação Técnica Completa
|
||||
|
||||
**Versão:** 1.0
|
||||
**Data:** Janeiro 2026
|
||||
**Desenvolvido por:** Arcádia Technology
|
||||
|
||||
---
|
||||
|
||||
## Sumário
|
||||
|
||||
1. [Visão Geral](#visão-geral)
|
||||
2. [Arquitetura do Sistema](#arquitetura-do-sistema)
|
||||
3. [Módulos do Sistema](#módulos-do-sistema)
|
||||
4. [Modelo de Dados](#modelo-de-dados)
|
||||
5. [APIs e Endpoints](#apis-e-endpoints)
|
||||
6. [Integrações Externas](#integrações-externas)
|
||||
7. [Segurança e Autenticação](#segurança-e-autenticação)
|
||||
8. [Guia de Implantação](#guia-de-implantação)
|
||||
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O **Arcádia Suite** é um Sistema Operacional Empresarial (Business Operating System) alimentado por Inteligência Artificial, projetado para revolucionar operações empresariais. O sistema integra cinco pilares fundamentais:
|
||||
|
||||
### Os 5 Pilares
|
||||
|
||||
1. **Knowledge Graph** - Grafo de conhecimento para dados empresariais interconectados
|
||||
2. **Central Intelligence (Scientist)** - Módulo de IA para geração automática de soluções
|
||||
3. **Manus (Agente Autônomo)** - Execução de tarefas e automação
|
||||
4. **Centro de Comunicação Unificado** - Interação com clientes via múltiplas plataformas
|
||||
5. **IDE Completa** - Ambiente de desenvolvimento multi-modal
|
||||
|
||||
### Segmentação de Produtos
|
||||
|
||||
| Produto | Público-Alvo | Stack Tecnológica |
|
||||
|---------|-------------|-------------------|
|
||||
| **Arcádia Plus** | Pequenas empresas | Node.js + Python + PostgreSQL |
|
||||
| **Arcádia Next** | Médias/Grandes empresas | Frappe Framework + PostgreSQL |
|
||||
|
||||
Ambos compartilham o **Arcádia Fisco** como motor fiscal centralizado.
|
||||
|
||||
---
|
||||
|
||||
## Arquitetura do Sistema
|
||||
|
||||
### Arquitetura em 4 Camadas
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CAMADA DE APRESENTAÇÃO │
|
||||
│ React 18 + TypeScript + Tailwind CSS + shadcn/ui │
|
||||
│ Interface estilo navegador com abas e omnibox │
|
||||
│ Porta: 5000 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CAMADA DE ORQUESTRAÇÃO │
|
||||
│ Express.js + Socket.IO + Manus Agent │
|
||||
│ API REST + WebSocket em tempo real │
|
||||
│ Porta: 5000 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CAMADA DE INTELIGÊNCIA │
|
||||
│ FastAPI (Python) + OpenAI API │
|
||||
│ Scientist, Embeddings, RPA, Workflows │
|
||||
│ Porta: 8001 (IA) / 8002 (Fisco) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CAMADA DE DADOS │
|
||||
│ PostgreSQL + Knowledge Graph + ChromaDB │
|
||||
│ Drizzle ORM + Session Store │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Estrutura de Diretórios
|
||||
|
||||
```
|
||||
arcadia-suite/
|
||||
├── client/ # Frontend React
|
||||
│ └── src/
|
||||
│ ├── components/ # Componentes reutilizáveis
|
||||
│ ├── hooks/ # React hooks customizados
|
||||
│ ├── lib/ # Utilitários e configurações
|
||||
│ └── pages/ # Páginas da aplicação
|
||||
├── server/ # Backend Node.js
|
||||
│ ├── admin/ # Rotas administrativas
|
||||
│ ├── api-central/ # Central de APIs
|
||||
│ ├── automations/ # Motor de automações
|
||||
│ ├── bi/ # Business Intelligence
|
||||
│ ├── chat/ # Chat interno
|
||||
│ ├── compass/ # Process Compass (clientes/projetos)
|
||||
│ ├── crm/ # Gestão de relacionamento
|
||||
│ ├── email/ # Serviço de e-mail
|
||||
│ ├── erp/ # Integração ERP
|
||||
│ ├── fisco/ # Motor fiscal (NF-e)
|
||||
│ ├── ide/ # IDE integrada
|
||||
│ ├── learning/ # Sistema de aprendizado
|
||||
│ ├── login-bridge/ # Bridge de autenticação
|
||||
│ ├── manus/ # Agente autônomo
|
||||
│ ├── production/ # Gestão de produção
|
||||
│ ├── productivity/ # Hub de produtividade
|
||||
│ ├── proxy/ # Proxy reverso
|
||||
│ ├── python/ # Serviços Python (FastAPI)
|
||||
│ ├── support/ # Central de suporte
|
||||
│ ├── valuation/ # Precificação e valuation
|
||||
│ └── whatsapp/ # Integração WhatsApp
|
||||
└── shared/ # Código compartilhado
|
||||
└── schema.ts # Schemas do banco de dados
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Módulos do Sistema
|
||||
|
||||
### 1. Cockpit (Dashboard Principal)
|
||||
**Arquivo:** `client/src/pages/Cockpit.tsx`
|
||||
|
||||
Painel central com visão geral do sistema:
|
||||
- Widgets configuráveis
|
||||
- Métricas em tempo real
|
||||
- Atividades recentes
|
||||
- Atalhos para módulos
|
||||
|
||||
### 2. Process Compass
|
||||
**Arquivo:** `client/src/pages/ProcessCompass.tsx`
|
||||
**API:** `/api/compass/*`
|
||||
|
||||
Gestão completa de processos empresariais:
|
||||
- **Clientes:** Cadastro, histórico, segmentação
|
||||
- **Projetos:** Cronograma, tarefas, milestones
|
||||
- **Contratos:** Gestão de contratos e renovações
|
||||
- **Timesheet:** Controle de horas trabalhadas
|
||||
|
||||
### 3. Comunicação Unificada
|
||||
**Arquivo:** `client/src/pages/Comunicacao.tsx`
|
||||
**API:** `/api/whatsapp/*`
|
||||
|
||||
Centro de comunicação multi-canal:
|
||||
- **WhatsApp Business:** Atendimento via Baileys
|
||||
- **Chat Interno:** Comunicação da equipe
|
||||
- **E-mail:** Integração IMAP/SMTP
|
||||
- **Tickets:** Sistema de filas de atendimento
|
||||
|
||||
### 4. CRM (Customer Relationship Management)
|
||||
**Arquivo:** `client/src/pages/Crm.tsx`
|
||||
**API:** `/api/crm/*`
|
||||
|
||||
Gestão de relacionamento com clientes:
|
||||
- Pipeline de vendas
|
||||
- Funil de conversão
|
||||
- Gestão de oportunidades
|
||||
- Comissionamento automático
|
||||
- Integração com Google Calendar
|
||||
|
||||
### 5. Business Intelligence (Arcádia Insights)
|
||||
**Arquivo:** `client/src/pages/BiWorkspace.tsx`
|
||||
**API:** `/api/bi/*`
|
||||
|
||||
Análise e visualização de dados:
|
||||
- Upload de arquivos (CSV, Excel)
|
||||
- Gráficos interativos (Recharts)
|
||||
- Dashboards personalizáveis
|
||||
- Conexão com múltiplas fontes
|
||||
|
||||
### 6. Scientist (Central de Inteligência)
|
||||
**Arquivo:** `client/src/pages/Scientist.tsx`
|
||||
**API:** `/api/scientist/*`
|
||||
|
||||
Módulo de auto-programação com IA:
|
||||
- Análise de dados automatizada
|
||||
- Geração de código (Python/SQL)
|
||||
- Execução em sandbox
|
||||
- Armazenamento de soluções reutilizáveis
|
||||
|
||||
### 7. Manus (Agente Autônomo)
|
||||
**Arquivo:** `client/src/pages/Agent.tsx`
|
||||
**API:** `/api/manus/*`
|
||||
|
||||
Executor de tarefas autônomo:
|
||||
- Loop pensamento-ação-observação
|
||||
- Ferramentas disponíveis:
|
||||
- Busca web
|
||||
- Consulta ao Knowledge Graph
|
||||
- Consulta ERP
|
||||
- Cálculos
|
||||
- Envio de mensagens
|
||||
- Geração de relatórios
|
||||
- Agendamentos
|
||||
|
||||
### 8. Arcádia Fisco (Motor Fiscal)
|
||||
**Arquivo:** `client/src/pages/Fisco.tsx`
|
||||
**API:** `/api/fisco/*`
|
||||
|
||||
Motor fiscal centralizado para compliance brasileiro:
|
||||
- **NCM:** Nomenclatura Comum do Mercosul
|
||||
- **CFOP:** Código Fiscal de Operações
|
||||
- **CEST:** Código Especificador da Substituição Tributária
|
||||
- **Grupos de Tributação:** Configuração de impostos
|
||||
- **Certificados Digitais:** Gestão de A1/A3
|
||||
- **NF-e/NFC-e:** Emissão de notas fiscais eletrônicas
|
||||
- **IBS/CBS:** Campos para Reforma Tributária
|
||||
|
||||
#### Integração nfelib (Python)
|
||||
**Arquivo:** `server/python/fisco_service.py`
|
||||
|
||||
Serviço FastAPI para processamento de NF-e:
|
||||
- Geração de XML (layout 4.00)
|
||||
- Assinatura digital com certificado A1
|
||||
- Comunicação com SEFAZ (homologação/produção)
|
||||
- Consulta, cancelamento e inutilização
|
||||
|
||||
### 9. Produção
|
||||
**Arquivo:** `client/src/pages/Production.tsx`
|
||||
**API:** `/api/production/*`
|
||||
|
||||
Gestão de produção e manufatura:
|
||||
- Ordens de produção
|
||||
- Controle de estoque
|
||||
- Rastreabilidade
|
||||
- Custos de produção
|
||||
|
||||
### 10. Valuation (Precificação)
|
||||
**Arquivo:** `client/src/pages/Valuation.tsx`
|
||||
**API:** `/api/valuation/*`
|
||||
|
||||
Sistema de precificação inteligente:
|
||||
- Cálculo de custos
|
||||
- Margem de contribuição
|
||||
- Markup
|
||||
- Simulações de preço
|
||||
|
||||
### 11. Suporte
|
||||
**Arquivo:** `client/src/pages/Support.tsx`
|
||||
**API:** `/api/support/*`
|
||||
|
||||
Central de atendimento:
|
||||
- Tickets de suporte
|
||||
- Base de conhecimento
|
||||
- SLA e prioridades
|
||||
- Histórico de atendimentos
|
||||
|
||||
### 12. Automações
|
||||
**Arquivo:** `client/src/pages/Automations.tsx`
|
||||
**API:** `/api/automations/*`
|
||||
|
||||
Motor de automações:
|
||||
- Triggers e ações
|
||||
- Workflows visuais
|
||||
- Integrações via webhooks
|
||||
- Agendamentos (cron)
|
||||
|
||||
### 13. Knowledge Base
|
||||
**Arquivo:** `client/src/pages/Knowledge.tsx`
|
||||
**API:** `/api/knowledge/*`
|
||||
|
||||
Base de conhecimento:
|
||||
- Artigos e documentação
|
||||
- Categorização
|
||||
- Busca semântica
|
||||
- Integração com IA
|
||||
|
||||
### 14. IDE
|
||||
**Arquivo:** `client/src/pages/IDE.tsx`
|
||||
**API:** `/api/ide/*`
|
||||
|
||||
Ambiente de desenvolvimento integrado:
|
||||
- Monaco Editor
|
||||
- Terminal (Xterm.js)
|
||||
- Execução de código
|
||||
- Gerenciamento de arquivos
|
||||
|
||||
### 15. Administração
|
||||
**Arquivo:** `client/src/pages/Admin.tsx`
|
||||
**API:** `/api/admin/*`
|
||||
|
||||
Painel administrativo:
|
||||
- **Usuários:** Gestão de contas
|
||||
- **Perfis:** Controle de acesso
|
||||
- **Parceiros:** Hierarquia multi-tenant
|
||||
- **Módulos:** Configuração de funcionalidades
|
||||
- **Configurações:** Parâmetros do sistema
|
||||
|
||||
### 16. API Hub
|
||||
**Arquivo:** `client/src/pages/ApiHub.tsx`
|
||||
|
||||
Documentação interativa de APIs:
|
||||
- Listagem de endpoints
|
||||
- Testes em tempo real
|
||||
- Exemplos de uso
|
||||
- Geração de código
|
||||
|
||||
---
|
||||
|
||||
## Modelo de Dados
|
||||
|
||||
### Entidades Principais
|
||||
|
||||
#### Usuários e Autenticação
|
||||
```sql
|
||||
users -- Usuários do sistema
|
||||
profiles -- Perfis de acesso
|
||||
roles -- Papéis (RBAC)
|
||||
permissions -- Permissões granulares
|
||||
role_permissions -- Associação papel-permissão
|
||||
user_roles -- Associação usuário-papel
|
||||
module_access -- Controle de acesso a módulos
|
||||
```
|
||||
|
||||
#### Produtividade
|
||||
```sql
|
||||
workspace_pages -- Páginas estilo Notion
|
||||
page_blocks -- Blocos de conteúdo
|
||||
page_links -- Links bidirecionais
|
||||
dashboard_widgets -- Widgets do dashboard
|
||||
quick_notes -- Notas rápidas
|
||||
activity_feed -- Feed de atividades
|
||||
user_favorites -- Favoritos
|
||||
command_history -- Histórico de comandos
|
||||
```
|
||||
|
||||
#### Conversação e IA
|
||||
```sql
|
||||
conversations -- Conversas com agente
|
||||
messages -- Mensagens
|
||||
chat_attachments -- Anexos
|
||||
knowledge_base -- Base de conhecimento
|
||||
```
|
||||
|
||||
#### ERP e Integrações
|
||||
```sql
|
||||
erp_connections -- Conexões com ERPs
|
||||
agent_tasks -- Tarefas do agente
|
||||
task_executions -- Execuções de tarefas
|
||||
```
|
||||
|
||||
#### Comunicação
|
||||
```sql
|
||||
chat_threads -- Threads de chat
|
||||
chat_participants -- Participantes
|
||||
chat_messages -- Mensagens de chat
|
||||
whatsapp_sessions -- Sessões WhatsApp
|
||||
whatsapp_contacts -- Contatos WhatsApp
|
||||
whatsapp_messages -- Mensagens WhatsApp
|
||||
whatsapp_queues -- Filas de atendimento
|
||||
whatsapp_tickets -- Tickets de atendimento
|
||||
```
|
||||
|
||||
#### Process Compass
|
||||
```sql
|
||||
compass_clients -- Clientes
|
||||
compass_projects -- Projetos
|
||||
compass_project_members -- Membros de projeto
|
||||
compass_project_phases -- Fases de projeto
|
||||
compass_contracts -- Contratos
|
||||
compass_timesheet -- Timesheet
|
||||
compass_invoices -- Faturas
|
||||
compass_payments -- Pagamentos
|
||||
```
|
||||
|
||||
#### CRM
|
||||
```sql
|
||||
crm_leads -- Leads
|
||||
crm_opportunities -- Oportunidades
|
||||
crm_activities -- Atividades
|
||||
crm_pipelines -- Pipelines
|
||||
crm_stages -- Estágios
|
||||
crm_commissions -- Comissões
|
||||
```
|
||||
|
||||
#### Fisco
|
||||
```sql
|
||||
fisco_ncm -- NCMs
|
||||
fisco_cest -- CESTs
|
||||
fisco_cfop -- CFOPs
|
||||
fisco_grupos_tributacao -- Grupos de tributação
|
||||
fisco_natureza_operacao -- Naturezas de operação
|
||||
fisco_ibpt -- Tabela IBPT
|
||||
fisco_certificados -- Certificados digitais
|
||||
fisco_configuracoes -- Configurações fiscais
|
||||
fisco_notas -- Notas fiscais
|
||||
fisco_nota_itens -- Itens das notas
|
||||
fisco_nota_eventos -- Eventos fiscais
|
||||
```
|
||||
|
||||
#### Multi-Tenant
|
||||
```sql
|
||||
partners -- Parceiros
|
||||
partner_invites -- Convites de parceiros
|
||||
tenant_clients -- Clientes dos tenants
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## APIs e Endpoints
|
||||
|
||||
### Estrutura Base
|
||||
|
||||
| Módulo | Base URL | Descrição |
|
||||
|--------|----------|-----------|
|
||||
| Admin | `/api/admin` | Administração do sistema |
|
||||
| Compass | `/api/compass` | Process Compass |
|
||||
| CRM | `/api/crm` | Gestão de relacionamento |
|
||||
| WhatsApp | `/api/whatsapp` | Comunicação WhatsApp |
|
||||
| Fisco | `/api/fisco` | Motor fiscal |
|
||||
| BI | `/api/bi` | Business Intelligence |
|
||||
| Production | `/api/production` | Gestão de produção |
|
||||
| Valuation | `/api/valuation` | Precificação |
|
||||
| Support | `/api/support` | Central de suporte |
|
||||
| Automations | `/api/automations` | Automações |
|
||||
| IDE | `/api/ide` | Ambiente de desenvolvimento |
|
||||
| Learning | `/api/learning` | Sistema de aprendizado |
|
||||
|
||||
### Exemplos de Endpoints
|
||||
|
||||
#### Fisco - NF-e
|
||||
```
|
||||
GET /api/fisco/nfe/service-status # Status do serviço
|
||||
POST /api/fisco/nfe/validar-certificado # Validar certificado A1
|
||||
POST /api/fisco/nfe/gerar-xml # Gerar XML preview
|
||||
POST /api/fisco/nfe/emitir # Emitir NF-e
|
||||
POST /api/fisco/nfe/consultar # Consultar na SEFAZ
|
||||
POST /api/fisco/nfe/cancelar # Cancelar NF-e
|
||||
POST /api/fisco/nfe/inutilizar # Inutilizar numeração
|
||||
```
|
||||
|
||||
#### Compass - Clientes
|
||||
```
|
||||
GET /api/compass/clients # Listar clientes
|
||||
GET /api/compass/clients/:id # Detalhes do cliente
|
||||
POST /api/compass/clients # Criar cliente
|
||||
PUT /api/compass/clients/:id # Atualizar cliente
|
||||
DELETE /api/compass/clients/:id # Excluir cliente
|
||||
```
|
||||
|
||||
#### Admin - Usuários
|
||||
```
|
||||
GET /api/admin/users # Listar usuários
|
||||
GET /api/admin/users/:id # Detalhes do usuário
|
||||
POST /api/admin/users # Criar usuário
|
||||
PUT /api/admin/users/:id # Atualizar usuário
|
||||
DELETE /api/admin/users/:id # Excluir usuário
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integrações Externas
|
||||
|
||||
### OpenAI API
|
||||
- **Uso:** Agente de IA, Scientist, auto-replies
|
||||
- **Modelo:** gpt-4o-mini
|
||||
- **Configuração:** Via Replit Secrets
|
||||
|
||||
### Baileys (WhatsApp)
|
||||
- **Uso:** Conexão multi-sessão WhatsApp
|
||||
- **Recursos:** QR Code, mensagens em tempo real
|
||||
- **Armazenamento:** Sessões no banco de dados
|
||||
|
||||
### nfelib (Python)
|
||||
- **Uso:** Emissão de NF-e/NFC-e
|
||||
- **Recursos:** XML, assinatura digital, SEFAZ
|
||||
- **Certificados:** A1 (PFX)
|
||||
|
||||
### Frappe Framework
|
||||
- **Uso:** Arcádia Next (futuro)
|
||||
- **Recursos:** ERPNext integration
|
||||
|
||||
### Google Calendar
|
||||
- **Uso:** Sincronização de eventos CRM
|
||||
- **OAuth:** Configurável por usuário
|
||||
|
||||
---
|
||||
|
||||
## Segurança e Autenticação
|
||||
|
||||
### Autenticação
|
||||
- **Método:** Session-based com Passport.js
|
||||
- **Hash:** bcrypt para senhas
|
||||
- **Sessões:** PostgreSQL session store
|
||||
|
||||
### Controle de Acesso (RBAC)
|
||||
```
|
||||
Hierarquia:
|
||||
├── Master (Arcádia)
|
||||
│ └── Parceiros
|
||||
│ └── Clientes
|
||||
```
|
||||
|
||||
### Permissões
|
||||
- Baseadas em módulos e ações
|
||||
- Código formato: `modulo.recurso.acao`
|
||||
- Exemplo: `compass.clients.write`
|
||||
|
||||
### Certificados Digitais
|
||||
- Tipo A1 (arquivo PFX)
|
||||
- Armazenamento seguro com senha
|
||||
- Validação de expiração
|
||||
|
||||
---
|
||||
|
||||
## Guia de Implantação
|
||||
|
||||
### Requisitos
|
||||
- Node.js 20+
|
||||
- Python 3.11+
|
||||
- PostgreSQL 15+
|
||||
- Certificado SSL (produção)
|
||||
|
||||
### Variáveis de Ambiente
|
||||
```env
|
||||
DATABASE_URL=postgresql://...
|
||||
SESSION_SECRET=...
|
||||
OPENAI_API_KEY=...
|
||||
FISCO_PYTHON_URL=http://localhost:8002
|
||||
FISCO_PORT=8002
|
||||
```
|
||||
|
||||
### Comandos de Inicialização
|
||||
```bash
|
||||
# Instalar dependências
|
||||
npm install
|
||||
|
||||
# Iniciar em desenvolvimento
|
||||
npm run dev
|
||||
|
||||
# Serviço Python Fisco (separado)
|
||||
cd server/python && python fisco_service.py
|
||||
```
|
||||
|
||||
### Portas
|
||||
| Serviço | Porta |
|
||||
|---------|-------|
|
||||
| Frontend + API | 5000 |
|
||||
| Python Fisco | 8002 |
|
||||
| Python IA | 8001 |
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### Janeiro 2026
|
||||
- Integração nfelib para NF-e
|
||||
- Módulo Fisco completo
|
||||
- Sistema de aprendizado automático
|
||||
- Validação Zod em todas as rotas fiscais
|
||||
|
||||
---
|
||||
|
||||
**Arcádia Suite** - Transformando a gestão empresarial com Inteligência Artificial
|
||||
|
||||
*Documentação gerada automaticamente pelo sistema.*
|
||||
|
|
@ -0,0 +1,705 @@
|
|||
# MAPA GERAL DO SISTEMA RETAIL - Arcádia Suite
|
||||
|
||||
---
|
||||
|
||||
## 1. VISÃO MACRO DO MÓDULO
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ARCÁDIA RETAIL - MAPA GERAL ║
|
||||
║ Loja de Celulares e Assistência Técnica ║
|
||||
╠══════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ║
|
||||
║ │DASHBOARD│ │ PDV │ │ PESSOAS │ │ ESTOQUE │ │SERVIÇOS │ ║
|
||||
║ │ (KPIs) │ │ (Caixa) │ │(Cadastro│ │(Depósito│ │ (O.S.) │ ║
|
||||
║ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ║
|
||||
║ │ │ │ │ │ ║
|
||||
║ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ║
|
||||
║ │TRADE-IN │ │ COMPRAS │ │CADASTROS│ │RELATÓRIO│ │COMISSÕES│ ║
|
||||
║ │(Aval.) │ │(Aquis.) │ │ (Config)│ │(Reports)│ │ (Metas) │ ║
|
||||
║ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ║
|
||||
║ │ │ │ │ │ ║
|
||||
║ └────────────┴────────────┴─────┬──────┴────────────┘ ║
|
||||
║ │ ║
|
||||
║ ┌─────────┴─────────┐ ║
|
||||
║ │ CONFIGURAÇÃO │ ║
|
||||
║ │ Plus / Empresas │ ║
|
||||
║ └───────────────────┘ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. FLUXO PRINCIPAL DE OPERAÇÕES
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ ENTRADA DE MERCADORIA │
|
||||
└──────────────────────┬───────────────────────┘
|
||||
│
|
||||
┌──────────────────────┬┴──────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ COMPRA │ │ TRADE-IN │ │ CONSIGNAÇÃO │
|
||||
│ Fornecedor │ │ Cliente │ │ Parceiro │
|
||||
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||
│ │ │
|
||||
│ ┌──────┴───────┐ │
|
||||
│ │ Avaliação │ │
|
||||
│ │ 19 itens │ │
|
||||
│ │ checklist │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌──────┴───────┐ │
|
||||
│ │ Aprovação │ │
|
||||
│ │ Gera Crédito│ │
|
||||
│ │ Cria O.S. │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌──────┴───────┐ │
|
||||
│ │ Revisão │ │
|
||||
│ │ O.S. Interna│ │
|
||||
│ │ Manutenção │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ ESTOQUE │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │Depósito │ │Depósito │ │Depósito │ │
|
||||
│ │ Loja │ │ Central │ │Trânsito │ │
|
||||
│ └────┬────┘ └────┬────┘ └────┬────┘ │
|
||||
│ └────────────┼────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────┬┴──────────────────┐ │
|
||||
│ │ Dispositivos │ Produtos │ │
|
||||
│ │ (por IMEI) │ (por qtd/série) │ │
|
||||
│ └────────────────┴───────────────────┘ │
|
||||
└────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ PDV │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||
│ │Dispositivos│ │ Produtos │ │ Faturar O.S. │ │
|
||||
│ │ (IMEI) │ │(Acessórios)│ │ (Serviços) │ │
|
||||
│ └─────┬──────┘ └─────┬──────┘ └──────┬───────┘ │
|
||||
│ └───────────────┼────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ CARRINHO │ │
|
||||
│ │ │ │
|
||||
│ │ Subtotal │ │
|
||||
│ │ - Desconto │ │
|
||||
│ │ - Trade-In │ │
|
||||
│ │ - Crédito │ │
|
||||
│ │ = TOTAL │ │
|
||||
│ └────────┬─────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ PAGAMENTO │ │
|
||||
│ │ 💵 Dinheiro │ │
|
||||
│ │ 💳 Débito │ │
|
||||
│ │ 💳 Crédito (Nx) │ │
|
||||
│ │ 📱 PIX │ │
|
||||
│ │ 🔀 Combinado │ │
|
||||
│ └────────┬─────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ VENDA │ │
|
||||
│ │ Impressão A4 │ │
|
||||
│ │ Sync Plus │ │
|
||||
│ │ NF-e/NFC-e │ │
|
||||
│ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────┼────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌─────────┐ ┌────────────┐
|
||||
│ COMISSÃO │ │RELATÓRIO│ │ DEVOLUÇÃO │
|
||||
│ Vendedor │ │Caixa │ │ Troca │
|
||||
│ Metas │ │Diário │ │ Crédito │
|
||||
└────────────┘ └─────────┘ └────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. CICLO DE VIDA DO DISPOSITIVO (IMEI)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ JORNADA DO DISPOSITIVO POR IMEI │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ENTRADA │
|
||||
│ ═══════ │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ COMPRA │ │ TRADE-IN │ │ CONSIGNAÇÃO │ │
|
||||
│ │ Fornecedor │ │ Cliente │ │ Parceiro │ │
|
||||
│ │ condition: │ │ condition: │ │ condition: │ │
|
||||
│ │ new │ │ used/refurb │ │ varies │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌──────┴───────┐ │ │
|
||||
│ │ │ AVALIAÇÃO │ │ │
|
||||
│ │ │ Checklist │ │ │
|
||||
│ │ │ 19 itens │ │ │
|
||||
│ │ └──────┬───────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌──────┴───────┐ │ │
|
||||
│ │ │ O.S. INT. │ │ │
|
||||
│ │ │ Revisão │ │ │
|
||||
│ │ │ Manutenção │ │ │
|
||||
│ │ └──────┬───────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ ESTOQUE (status: in_stock) │ │
|
||||
│ │ │ │
|
||||
│ │ IMEI: 35XXXXXXXXXXXXX │ │
|
||||
│ │ Marca: Samsung | Modelo: S24 Ultra │ │
|
||||
│ │ Cor: Preto | Storage: 256GB | RAM: 12GB │ │
|
||||
│ │ Condição: refurbished │ │
|
||||
│ │ Preço Compra: R$ 2.500 | Preço Venda: R$ 3.500 │ │
|
||||
│ │ Depósito: Loja Centro │ │
|
||||
│ └──────────────────────┬──────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ MOVIMENTAÇÕES │ │
|
||||
│ ═══════════════ │ │
|
||||
│ │ │
|
||||
│ ┌──────────┐ ┌──────┴──────┐ ┌──────────────┐ │
|
||||
│ │TRANSFER. │◄───│ DISPONÍVEL │───►│ ASSISTÊNCIA │ │
|
||||
│ │Entre lojas│ │ para venda │ │ O.S. cliente│ │
|
||||
│ │in_transit │ │ in_stock │ │ in_service │ │
|
||||
│ └──────────┘ └──────┬──────┘ └──────┬───────┘ │
|
||||
│ │ │ │
|
||||
│ │ │ (retorna após reparo) │
|
||||
│ │ └──────────┐ │
|
||||
│ │ │ │
|
||||
│ SAÍDA ▼ ▼ │
|
||||
│ ═════ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ VENDA │ │ DE VOLTA AO │ │
|
||||
│ │ PDV + NF-e │ │ ESTOQUE │ │
|
||||
│ │ status:sold │ │ in_stock │ │
|
||||
│ └──────┬───────┘ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────┴───────┐ │
|
||||
│ │ DEVOLUÇÃO? │ │
|
||||
│ │ returned │──────────► Volta ao estoque │
|
||||
│ └──────────────┘ │
|
||||
│ │
|
||||
│ RASTREAMENTO: device_history + imei_history (kardex completo) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. FLUXO DO TRADE-IN (4 ETAPAS DETALHADAS)
|
||||
|
||||
```
|
||||
╔═══════════════════════════════════════════════════════════════════════════╗
|
||||
║ TRADE-IN - FLUXO COMPLETO ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ ETAPA 1: AVALIAÇÃO ║
|
||||
║ ══════════════════ ║
|
||||
║ ║
|
||||
║ ┌────────────────────────────────────────────────────────────┐ ║
|
||||
║ │ Cliente chega com dispositivo usado │ ║
|
||||
║ │ │ ║
|
||||
║ │ 1. Identificar cliente (busca por nome/CPF) │ ║
|
||||
║ │ 2. Registrar IMEI, Marca, Modelo │ ║
|
||||
║ │ 3. Preencher Checklist (19 itens): │ ║
|
||||
║ │ │ ║
|
||||
║ │ ┌──────────────────────────────────────────────────────┐ │ ║
|
||||
║ │ │ ☐ Liga corretamente ☐ Sensores/NFC │ │ ║
|
||||
║ │ │ ☐ Sem avarias/fantasma ☐ Face ID/Touch ID │ │ ║
|
||||
║ │ │ ☐ Sem manchas na tela ☐ Microfones │ │ ║
|
||||
║ │ │ ☐ Botões funcionando ☐ Áudio auricular │ │ ║
|
||||
║ │ │ ☐ Marcas de uso ☐ Alto-falante │ │ ║
|
||||
║ │ │ ☐ Wi-Fi ☐ Carregamento │ │ ║
|
||||
║ │ │ ☐ Chip ☐ Câmeras │ │ ║
|
||||
║ │ │ ☐ 4G/5G ☐ Flash │ │ ║
|
||||
║ │ │ ☐ Possui carregador ☐ 3uTools OK │ │ ║
|
||||
║ │ │ 🔋 Bateria: ____% │ │ ║
|
||||
║ │ └──────────────────────────────────────────────────────┘ │ ║
|
||||
║ │ │ ║
|
||||
║ │ 4. Listar peças necessárias (se houver) │ ║
|
||||
║ │ 5. Definir valor estimado │ ║
|
||||
║ │ 6. Assinatura digital do cliente │ ║
|
||||
║ │ 7. Imprimir comprovante │ ║
|
||||
║ │ │ ║
|
||||
║ │ Status: PENDING │ ║
|
||||
║ └────────────────────────────────────────────┬───────────────┘ ║
|
||||
║ │ ║
|
||||
║ ▼ ║
|
||||
║ ETAPA 2: APROVAÇÃO ║
|
||||
║ ══════════════════ ║
|
||||
║ ║
|
||||
║ ┌────────────────────────────────────────────────────────────┐ ║
|
||||
║ │ Gerente revisa avaliação e decide: │ ║
|
||||
║ │ │ ║
|
||||
║ │ [APROVAR] [REJEITAR] │ ║
|
||||
║ │ │ │ │ ║
|
||||
║ │ ▼ ▼ │ ║
|
||||
║ │ Automaticamente: Avaliação │ ║
|
||||
║ │ ✅ Gera CRÉDITO para cliente rejeitada. │ ║
|
||||
║ │ (R$ do valor estimado) Sem ações. │ ║
|
||||
║ │ ✅ Cria O.S. INTERNA │ ║
|
||||
║ │ (INT2602XXXXXX) │ ║
|
||||
║ │ ✅ Registra no IMEI History │ ║
|
||||
║ │ │ ║
|
||||
║ │ Status: APPROVED │ ║
|
||||
║ └────────────────────────────────────────────┬───────────────┘ ║
|
||||
║ │ ║
|
||||
║ ▼ ║
|
||||
║ ETAPA 3: REVISÃO / MANUTENÇÃO ║
|
||||
║ ═════════════════════════════ ║
|
||||
║ ║
|
||||
║ ┌────────────────────────────────────────────────────────────┐ ║
|
||||
║ │ Técnico trabalha na O.S. Interna: │ ║
|
||||
║ │ │ ║
|
||||
║ │ 1. Diagnóstico técnico │ ║
|
||||
║ │ 2. Troca de peças (registra peças + custos) │ ║
|
||||
║ │ 3. Limpeza e preparação │ ║
|
||||
║ │ 4. Teste de qualidade │ ║
|
||||
║ │ 5. Preenche checklist de conclusão │ ║
|
||||
║ │ 6. Finaliza O.S. Interna │ ║
|
||||
║ │ │ ║
|
||||
║ │ Custos: │ ║
|
||||
║ │ ├── Peças utilizadas: R$ XXX │ ║
|
||||
║ │ ├── Mão de obra: R$ XXX │ ║
|
||||
║ │ └── Total reparo: R$ XXX │ ║
|
||||
║ │ │ ║
|
||||
║ │ Status O.S.: COMPLETED │ ║
|
||||
║ └────────────────────────────────────────────┬───────────────┘ ║
|
||||
║ │ ║
|
||||
║ ▼ ║
|
||||
║ ETAPA 4: ENTRADA NO ESTOQUE ║
|
||||
║ ═══════════════════════════ ║
|
||||
║ ║
|
||||
║ ┌────────────────────────────────────────────────────────────┐ ║
|
||||
║ │ Ao finalizar O.S. Interna: │ ║
|
||||
║ │ │ ║
|
||||
║ │ ✅ Cria dispositivo no estoque (mobile_devices) │ ║
|
||||
║ │ condition: "refurbished" │ ║
|
||||
║ │ acquisitionType: "trade_in" │ ║
|
||||
║ │ │ ║
|
||||
║ │ 💰 Cálculo do preço sugerido: │ ║
|
||||
║ │ Custo aquisição = Valor Trade-In + Custo Reparo │ ║
|
||||
║ │ Preço sugerido = Custo × (1 + Margem%) │ ║
|
||||
║ │ Ex: R$1500 + R$300 = R$1800 × 1.30 = R$ 2.340 │ ║
|
||||
║ │ │ ║
|
||||
║ │ ✅ Registra no IMEI History │ ║
|
||||
║ │ ✅ Registra no Activity Feed │ ║
|
||||
║ │ ✅ Disponível no PDV para venda │ ║
|
||||
║ │ │ ║
|
||||
║ │ Status: IN_STOCK (pronto para venda) │ ║
|
||||
║ └────────────────────────────────────────────────────────────┘ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. MAPA DO PDV (PONTO DE VENDA)
|
||||
|
||||
```
|
||||
╔═══════════════════════════════════════════════════════════════════════╗
|
||||
║ PDV - PONTO DE VENDA ║
|
||||
╠═══════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ BARRA SUPERIOR ║
|
||||
║ ┌──────────────────────────────────────────────────────────────────┐ ║
|
||||
║ │ 🏪 Empresa: Loja Centro 👤 Vendedor: João Silva │ ║
|
||||
║ │ │ ║
|
||||
║ │ [Sangria] [Reforço] [Devolução] [Selecionar Cliente] [Limpar] │ ║
|
||||
║ └──────────────────────────────────────────────────────────────────┘ ║
|
||||
║ ║
|
||||
║ ┌───────────────────────────────────┬──────────────────────────────┐ ║
|
||||
║ │ CATÁLOGO (3 colunas) │ CARRINHO (2 colunas) │ ║
|
||||
║ │ │ │ ║
|
||||
║ │ ┌──────────┬──────────┬────────┐ │ ┌──────────────────────────┐│ ║
|
||||
║ │ │📱Disposit│📦Produtos│🔧Fat.OS│ │ │ Samsung S24 Ultra ││ ║
|
||||
║ │ └──────────┴──────────┴────────┘ │ │ IMEI: 35XXX 📱Celular ││ ║
|
||||
║ │ │ │ R$ 3.500,00 ││ ║
|
||||
║ │ [🔍 Buscar... ] │ ├──────────────────────────┤│ ║
|
||||
║ │ │ │ Capinha Silicone ││ ║
|
||||
║ │ ┌──────────────────────────────┐ │ │ 2x R$ 45,00 ││ ║
|
||||
║ │ │ Samsung S24 Ultra │ │ │ R$ 90,00 ││ ║
|
||||
║ │ │ 256GB | Preto | IMEI: 35XX │ │ ├──────────────────────────┤│ ║
|
||||
║ │ │ 🟢 Novo 📦 Loja Centro │ │ │ O.S. #OS2602ABC ││ ║
|
||||
║ │ │ R$ 3.500,00 │ │ │ Troca de tela 🔧Serviço ││ ║
|
||||
║ │ │ [+ Adicionar] │ │ │ R$ 450,00 ││ ║
|
||||
║ │ ├──────────────────────────────┤ │ ╞══════════════════════════╡│ ║
|
||||
║ │ │ iPhone 15 Pro │ │ │ Subtotal R$ 4.040,00 ││ ║
|
||||
║ │ │ 128GB | Branco | IMEI: 86XX │ │ │ Desconto -R$ 40,00 ││ ║
|
||||
║ │ │ 🔵 Recond 📦 Loja Centro │ │ │ Trade-In -R$ 1.500,00 ││ ║
|
||||
║ │ │ R$ 4.200,00 │ │ │ Crédito -R$ 200,00 ││ ║
|
||||
║ │ │ [+ Adicionar] │ │ │─────────────────────────││ ║
|
||||
║ │ ├──────────────────────────────┤ │ │ TOTAL R$ 2.300,00 ││ ║
|
||||
║ │ │ Xiaomi 14 │ │ │ ││ ║
|
||||
║ │ │ 512GB | Azul | IMEI: 86XX │ │ │ 💙 Crédito: R$ 200,00 ││ ║
|
||||
║ │ │ 🟡 Usado 📦 Loja Centro │ │ │ ││ ║
|
||||
║ │ │ R$ 2.800,00 │ │ │ [↕️ Trade-In] ││ ║
|
||||
║ │ │ [+ Adicionar] │ │ │ [💰 FINALIZAR VENDA] ││ ║
|
||||
║ │ └──────────────────────────────┘ │ └──────────────────────────┘│ ║
|
||||
║ └───────────────────────────────────┴──────────────────────────────┘ ║
|
||||
║ ║
|
||||
║ MODAL DE PAGAMENTO ║
|
||||
║ ┌──────────────────────────────────────────────────────────────────┐ ║
|
||||
║ │ Total: R$ 2.300,00 │ ║
|
||||
║ │ │ ║
|
||||
║ │ [Dinheiro ✓] [Débito] [Crédito] [PIX] [Combinado] │ ║
|
||||
║ │ │ ║
|
||||
║ │ Valor: [R$ 2.300,00] Parcelas: [1x] │ ║
|
||||
║ │ │ ║
|
||||
║ │ Desconto: [___]% ou [R$ ___] │ ║
|
||||
║ │ │ ║
|
||||
║ │ [CONFIRMAR PAGAMENTO] │ ║
|
||||
║ └──────────────────────────────────────────────────────────────────┘ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. FLUXO DA ORDEM DE SERVIÇO (12 ESTADOS)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ORDEM DE SERVIÇO - FLUXO DE STATUS │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────┐ │
|
||||
│ │ ABERTA │ ◄── Cliente entrega dispositivo │
|
||||
│ │ open │ │
|
||||
│ └────┬─────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ DIAGNÓSTICO │ ◄── Técnico analisa problema │
|
||||
│ │ diagnosis │ │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ ORÇAMENTO │ ◄── Técnico elabora orçamento │
|
||||
│ │ quote │ │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ AGUARD. APROVAÇÃO │ ◄── Enviado para cliente │
|
||||
│ │ pending_approval │ │
|
||||
│ └─────┬────────┬─────┘ │
|
||||
│ │ │ │
|
||||
│ APROVOU │ │ REJEITOU │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ APROVADA │ │ REJEITADA│ ──► FIM │
|
||||
│ │ approved │ │ rejected │ │
|
||||
│ └────┬─────┘ └──────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────┐ │
|
||||
│ │ EM REPARO│ ◄── Técnico inicia trabalho │
|
||||
│ │in_repair │ │
|
||||
│ └────┬─────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────┼──────────┐ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ ┌──────────────┐ │ │
|
||||
│ │ AGUARD. PEÇAS│ │ ◄── Se precisar de peça │
|
||||
│ │waiting_parts │───────────────┘ │
|
||||
│ └──────────────┘ (peça chegou, volta para reparo) │
|
||||
│ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ QUALIDADE │ ◄── Verificação pós-reparo │
|
||||
│ │quality_check │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ PRONTO PARA │ ◄── Disponível para retirada │
|
||||
│ │ RETIRADA │ (aparece no PDV p/ faturar) │
|
||||
│ │ ready_pickup │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ CONCLUÍDA │ ◄── Cliente retirou + pagou │
|
||||
│ │ completed │ (faturada no PDV) │
|
||||
│ └──────────────┘ │
|
||||
│ │
|
||||
│ Em qualquer momento: ──────────► ┌──────────────┐ │
|
||||
│ │ CANCELADA │ │
|
||||
│ │ cancelled │ │
|
||||
│ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. MAPA DE RELATÓRIOS
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ RELATÓRIOS DO RETAIL │
|
||||
├──────────────┬──────────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ OS POR │ Status │ Qtd │ Valor Total │
|
||||
│ STATUS │ open │ 12 │ R$ 5.400 │
|
||||
│ │ in_repair │ 8 │ R$ 3.200 │
|
||||
│ │ completed │ 45 │ R$ 22.500 │
|
||||
│ │ │
|
||||
├──────────────┼──────────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ OS POR │ Técnico │ Total │ Concl. │ Andamento │ Receita │
|
||||
│ TÉCNICO │ Carlos │ 15 │ 12 │ 3 │ R$8.500 │
|
||||
│ │ Pedro │ 10 │ 8 │ 2 │ R$5.200 │
|
||||
│ │ │
|
||||
├──────────────┼──────────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ VENDAS POR │ Vendedor │ Vendas │ Receita │ Ticket │ Dias │
|
||||
│ VENDEDOR │ João │ 28 │ R$45.000 │ R$1607 │ 22 │
|
||||
│ │ Maria │ 22 │ R$38.000 │ R$1727 │ 20 │
|
||||
│ │ │
|
||||
├──────────────┼──────────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ MARGEM │ Dispositivo │ IMEI │ Custo │ Venda │ Margem │
|
||||
│ POR IMEI │ S24 Ultra │ 35XXX │ R$2500 │ R$3500 │ 40.0% │
|
||||
│ │ iPhone 15 │ 86XXX │ R$3800 │ R$4200 │ 10.5% │
|
||||
│ │ │
|
||||
├──────────────┼──────────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ CAIXA │ ┌────────────────────────────────────────────┐ │
|
||||
│ DIÁRIO │ │ Total Vendas: R$ 12.500 (8 vendas) │ │
|
||||
│ │ │ Dinheiro: R$ 4.200 │ Cartão: R$ 5.800 │ │
|
||||
│ │ │ PIX: R$ 2.500 │ Combinado: R$ 0 │ │
|
||||
│ (4 queries) │ │ Sangrias: -R$ 500 │ Reforços: +R$ 200 │ │
|
||||
│ │ │ Saldo Caixa: R$ 3.900 │ │
|
||||
│ │ └────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ POR VENDEDOR: │
|
||||
│ │ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
|
||||
│ │ │Vendedor│Vendas│Dinh. │Déb. │Créd.│ PIX │TOTAL │ │
|
||||
│ │ │João │ 5 │R$2100│R$1500│R$800│R$1200│R$5600│ │
|
||||
│ │ │Maria │ 3 │R$2100│R$1300│R$600│R$1300│R$6900│ │
|
||||
│ │ │TOTAL │ 8 │R$4200│R$2800│R$1400│R$2500│R$12.5│ │
|
||||
│ │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
|
||||
│ │ │
|
||||
├──────────────┼──────────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ GIRO DE │ Produto │ Estoque │ Vendas 30d │ Turnover │
|
||||
│ ESTOQUE │ Capinha │ 50 │ 35 │ 0.70 │
|
||||
│ │ Película │ 80 │ 42 │ 0.53 │
|
||||
│ │ │
|
||||
└──────────────┴──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. ARQUITETURA TÉCNICA
|
||||
|
||||
```
|
||||
╔═══════════════════════════════════════════════════════════════════════╗
|
||||
║ ARQUITETURA DO MÓDULO RETAIL ║
|
||||
╠═══════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ FRONTEND (React + TypeScript + Tailwind + shadcn/ui) ║
|
||||
║ ═══════════════════════════════════════════════════ ║
|
||||
║ ║
|
||||
║ client/src/pages/ArcadiaRetail.tsx ──── 10.067 linhas ║
|
||||
║ │ ║
|
||||
║ ├── Dashboard (KPIs, Feed, Alertas) ║
|
||||
║ ├── PDV (Carrinho, Catálogo, Pagamento) ║
|
||||
║ ├── Pessoas (CRUD, Papéis, Histórico) ║
|
||||
║ ├── Estoque (Depósitos, Séries, Inventário) ║
|
||||
║ ├── Serviços (O.S. CRUD, Checklist) ║
|
||||
║ ├── Trade-In (Avaliações, Fluxo 4 etapas) ║
|
||||
║ ├── Compras (Pedidos, Recebimento) ║
|
||||
║ ├── Cadastros (Pagamento, Vendedores, Promoções) ║
|
||||
║ ├── Relatórios (6 sub-abas) ║
|
||||
║ ├── Comissões (Dashboard, Metas, Fechamento) ║
|
||||
║ └── Configuração (Plus, Empresas, Sync) ║
|
||||
║ ║
|
||||
║ client/src/components/TradeInForm.tsx ─── 988 linhas ║
|
||||
║ └── Formulário completo c/ checklist + assinatura ║
|
||||
║ ║
|
||||
║ ───────────────────────────────────────────────────────────── ║
|
||||
║ API (fetch → /api/retail/*) ║
|
||||
║ ───────────────────────────────────────────────────────────── ║
|
||||
║ ║
|
||||
║ BACKEND (Express.js + Drizzle ORM) ║
|
||||
║ ══════════════════════════════════ ║
|
||||
║ ║
|
||||
║ server/retail/routes.ts ──── 5.218 linhas (~130 endpoints) ║
|
||||
║ │ ║
|
||||
║ ├── CRUD: Devices, Evaluations, ServiceOrders, ║
|
||||
║ │ Persons, Products, Warehouses, etc. ║
|
||||
║ ├── PDV: Sessions, Sales, Payments, CashMovements ║
|
||||
║ ├── Trade-In: Approve, Process, FullFlow, Stock ║
|
||||
║ ├── Reports: OsStatus, OsTech, SalesSeller, ║
|
||||
║ │ MarginIMEI, DailyCash, StockTurnover ║
|
||||
║ ├── Returns: Search, Process, Credits ║
|
||||
║ └── Commissions: Plans, Goals, Calculate, Close ║
|
||||
║ ║
|
||||
║ server/retail/plus-sync.ts ──── 542 linhas ║
|
||||
║ └── Sync: Customers, Sales+Items, NF-e ║
|
||||
║ ║
|
||||
║ ───────────────────────────────────────────────────────────── ║
|
||||
║ Drizzle ORM → PostgreSQL ║
|
||||
║ ───────────────────────────────────────────────────────────── ║
|
||||
║ ║
|
||||
║ BANCO DE DADOS (PostgreSQL + 40 tabelas) ║
|
||||
║ ════════════════════════════════════════ ║
|
||||
║ ║
|
||||
║ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ║
|
||||
║ │ Dispositivos │ │ Vendas │ │ Estoque │ ║
|
||||
║ │ mobile_devices│ │ pos_sales │ │ warehouses │ ║
|
||||
║ │ device_history│ │pos_sale_items│ │warehouse_stk │ ║
|
||||
║ │ imei_history │ │pos_sessions │ │stock_movem. │ ║
|
||||
║ └──────────────┘ │cash_movements│ │transfers │ ║
|
||||
║ └──────────────┘ │serials │ ║
|
||||
║ ┌──────────────┐ │inventories │ ║
|
||||
║ │ Trade-In │ ┌──────────────┐ └──────────────┘ ║
|
||||
║ │ evaluations │ │ Serviços │ ║
|
||||
║ │ checklist_* │ │service_orders│ ┌──────────────┐ ║
|
||||
║ │ acquisitions │ │ so_items │ │ Cadastros │ ║
|
||||
║ │ transfer_doc │ │ warranties │ │payment_meth. │ ║
|
||||
║ └──────────────┘ └──────────────┘ │ sellers │ ║
|
||||
║ │ comm_plans │ ║
|
||||
║ ┌──────────────┐ ┌──────────────┐ │ promotions │ ║
|
||||
║ │ Créditos │ │ Devoluções │ │ price_tables │ ║
|
||||
║ │customer_cred.│ │return_exch. │ │product_types │ ║
|
||||
║ └──────────────┘ │return_items │ └──────────────┘ ║
|
||||
║ └──────────────┘ ║
|
||||
║ ║
|
||||
║ ───────────────────────────────────────────────────────────── ║
|
||||
║ ║
|
||||
║ INTEGRAÇÃO PLUS (Laravel ERP - Porta 8080) ║
|
||||
║ ══════════════════════════════════════════ ║
|
||||
║ ║
|
||||
║ ┌───────────┐ ┌───────────┐ ┌───────────┐ ║
|
||||
║ │Proxy Rev. │ │ SSO │ │Auto-Start │ ║
|
||||
║ │/plus/* → │ │HMAC-SHA256│ │php artisan│ ║
|
||||
║ │:8080 │ │Token │ │serve │ ║
|
||||
║ └───────────┘ └───────────┘ └───────────┘ ║
|
||||
║ │ ║
|
||||
║ ▼ ║
|
||||
║ ┌─────────────────────────────────────────────────┐ ║
|
||||
║ │ Plus (Laravel) │ ║
|
||||
║ │ ├── NF-e / NFC-e (SEFAZ) │ ║
|
||||
║ │ ├── Clientes / Fornecedores │ ║
|
||||
║ │ ├── Vendas / Faturamento │ ║
|
||||
║ │ └── Fiscal completo │ ║
|
||||
║ └─────────────────────────────────────────────────┘ ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. SEGURANÇA MULTI-TENANT
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ CAMADAS DE SEGURANÇA │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ CAMADA 1: AUTENTICAÇÃO │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Passport.js Session → req.isAuthenticated() │ │
|
||||
│ │ Todas as rotas /api/retail/* exigem login │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ CAMADA 2: TENANT SCOPING │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ req.user.tenantId → obrigatório │ │
|
||||
│ │ Se ausente → 403 "Tenant not identified" │ │
|
||||
│ │ WHERE tenant_id = $tenantId em TODAS as queries │ │
|
||||
│ │ Dados de um tenant NUNCA vazam para outro │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ CAMADA 3: MODULE GATING │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ requireModule('retail') → verifica tenants.features.retail │ │
|
||||
│ │ Se módulo inativo → 403 "Módulo não habilitado" │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ CAMADA 4: OPERAÇÕES SENSÍVEIS │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Devoluções → Exige senha do GERENTE │ │
|
||||
│ │ Sangrias → Registra responsável + autorizador │ │
|
||||
│ │ Exclusões → Soft delete quando possível │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ CAMADA 5: AUDITORIA │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ retail_activity_feed → Todas as operações relevantes │ │
|
||||
│ │ device_history → Movimentação de IMEI │ │
|
||||
│ │ imei_history → Kardex completo por IMEI │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. MAPA DE INTEGRAÇÕES
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ INTEGRAÇÕES DO RETAIL │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ ARCÁDIA RETAIL │ │
|
||||
│ │ (Express) │ │
|
||||
│ └────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────┼───────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ PLUS (PHP) │ │ ERPNEXT │ │ POSTGRESQL │ │
|
||||
│ │ :8080 │ │ (API ext.) │ │ (Drizzle) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ▸ NF-e/NFC-e│ │ ▸ Clientes │ │ ▸ 40+ tabelas│ │
|
||||
│ │ ▸ Clientes │ │ ▸ Fornecedor │ │ ▸ Retail │ │
|
||||
│ │ ▸ Vendas │ │ ▸ Estoque │ │ ▸ Multi-ten. │ │
|
||||
│ │ ▸ Fiscal │ │ ▸ Financeiro │ │ │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ SEFAZ │ │
|
||||
│ │ NF-e / NFC-e (via Plus Laravel + nfelib) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Métricas Finais:**
|
||||
- **16.800+ linhas** de código
|
||||
- **40+ tabelas** no banco
|
||||
- **~130 endpoints** REST
|
||||
- **11 abas** na interface
|
||||
- **19 itens** no checklist de avaliação
|
||||
- **12 estados** de O.S.
|
||||
- **4 etapas** no fluxo de Trade-In
|
||||
- **5 formas** de pagamento
|
||||
- **4 queries** no caixa diário
|
||||
|
|
@ -0,0 +1,676 @@
|
|||
# Arcádia Plus - Mapa Completo dentro do Arcádia Suite
|
||||
|
||||
## O que é o Plus?
|
||||
|
||||
O **Arcádia Plus** é o **Motor de Execução Fiscal e Operacional** da plataforma Arcádia Suite. Enquanto o Arcádia Suite pensa, governa e orienta, o Plus executa, registra e obedece. É um ERP completo construído em Laravel/PHP que serve como backend fiscal para emissão de documentos eletrônicos (NF-e, NFC-e, CT-e, MDF-e) e operações comerciais.
|
||||
|
||||
---
|
||||
|
||||
## Posição na Arquitetura
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ ARCÁDIA SUITE (Cérebro) │
|
||||
│ Express.js - Porta 5000 │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ SOE - Sistema Operacional Empresarial │ │
|
||||
│ │ Domínios: Pessoas, Produtos, Vendas, Financeiro, Fiscal │ │
|
||||
│ │ Decisão + Governança + IA │ │
|
||||
│ └────────────────────┬───────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ SSO + Proxy (/plus) │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ARCÁDIA PLUS (Motor de Execução) │ │
|
||||
│ │ Laravel/PHP - Porta 8080 │ │
|
||||
│ │ Execução: NFe, NFCe, CTe, MDFe, PDV, Estoque, OS │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Outros Motores: │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
|
||||
│ │Contábil │ │ Fiscal │ │ BI │ │Automação │ │ Comm ││
|
||||
│ │ 8003 │ │ 8002 │ │ 8004 │ │ 8005 │ │ 8006 ││
|
||||
│ │ Python │ │ Python │ │ Python │ │ Python │ │ Node.js ││
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘│
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integração Suite ↔ Plus
|
||||
|
||||
### 1. Proxy Reverso (`server/plus/proxy.ts`)
|
||||
|
||||
O Arcádia Suite atua como gateway. Todas as requisições para `/plus/*` são redirecionadas para o Laravel na porta 8080:
|
||||
|
||||
```
|
||||
Navegador → /plus/vendas → Suite (5000) → Proxy → Laravel (8080/vendas)
|
||||
Navegador → /plus/nfe → Suite (5000) → Proxy → Laravel (8080/nfe)
|
||||
```
|
||||
|
||||
**Comportamentos do proxy:**
|
||||
- `/plus` ou `/plus/` → redireciona para `/empresas` (home do Laravel)
|
||||
- `/plus/home` → redireciona para `/empresas`
|
||||
- Headers `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Forwarded-Prefix` repassados
|
||||
- Respostas Location reescritas para manter o prefixo `/plus`
|
||||
- Timeout: 60 segundos
|
||||
- Status endpoint: `GET /api/plus/status`
|
||||
|
||||
### 2. SSO - Single Sign-On (`plus/app/Http/Controllers/Auth/SsoController.php`)
|
||||
|
||||
Login unificado: o usuário entra pelo Arcádia Suite e é autenticado automaticamente no Plus.
|
||||
|
||||
```
|
||||
┌─────────────────┐ Token JWT (Base64) ┌──────────────────┐
|
||||
│ Arcádia Suite │ ──────────────────────────▶ │ Arcádia Plus │
|
||||
│ (Node.js) │ sig = HMAC-SHA256 │ (Laravel) │
|
||||
│ │ ts = timestamp │ │
|
||||
│ Gera token com: │ │ Valida: │
|
||||
│ - email │ │ - iss = arcadia │
|
||||
│ - name │ │ - aud = plus │
|
||||
│ - role │ │ - timestamp ±5m │
|
||||
│ - username │ │ - HMAC signature │
|
||||
└─────────────────┘ └──────────────────┘
|
||||
│
|
||||
▼
|
||||
Auto-cria usuário
|
||||
se não existir
|
||||
```
|
||||
|
||||
**Segurança SSO:**
|
||||
- Segredo compartilhado: `SSO_SECRET` (variável de ambiente)
|
||||
- Expiração do token: 5 minutos (300 segundos)
|
||||
- Assinatura: HMAC-SHA256
|
||||
- Validação de issuer (`arcadia-suite`) e audience (`arcadia-plus`)
|
||||
- Provisionamento automático: se o email não existe no Plus, cria o usuário
|
||||
|
||||
### 3. Launcher (`server/plus/launcher.ts`)
|
||||
|
||||
O Suite inicia o Laravel automaticamente na inicialização:
|
||||
|
||||
```
|
||||
Suite inicia → Verifica PHP disponível → spawn php artisan serve --port=8080
|
||||
│
|
||||
┌───────┴───────┐
|
||||
│ PHP existe? │
|
||||
├── Sim → Inicia│
|
||||
└── Não → Pula │
|
||||
```
|
||||
|
||||
**Verificações:**
|
||||
- `which php` - PHP instalado?
|
||||
- `plus/artisan` existe?
|
||||
- Timeout: 15 segundos para iniciar
|
||||
|
||||
---
|
||||
|
||||
## Números do Plus
|
||||
|
||||
| Métrica | Quantidade |
|
||||
|---------|-----------|
|
||||
| Controllers | 376 |
|
||||
| Models | 320 |
|
||||
| Services | 12 |
|
||||
| Views (Blade) | 1.194 |
|
||||
| Migrations | 260 |
|
||||
| Rotas Web | 912 |
|
||||
| Rotas API | 634 |
|
||||
| **Total de Rotas** | **1.546** |
|
||||
|
||||
---
|
||||
|
||||
## Módulos do Plus - Detalhamento Completo
|
||||
|
||||
### 1. FISCAL - Documentos Eletrônicos
|
||||
|
||||
O coração do Plus. Emissão e gestão de todos os documentos fiscais brasileiros.
|
||||
|
||||
#### NF-e (Nota Fiscal Eletrônica)
|
||||
| Componente | Arquivo |
|
||||
|-----------|---------|
|
||||
| Controller Web | `NfeController.php` |
|
||||
| Controller API | `API/NFeController.php` |
|
||||
| Painel API | `API/NFePainelController.php` |
|
||||
| Service | `NFeService.php` |
|
||||
| Service API | `NFeServiceApi.php` |
|
||||
| Model | `Nfe.php`, `ItemNfe.php`, `FaturaNfe.php` |
|
||||
| Views | `nfe/*.blade.php` |
|
||||
| XML | `NfeXmlController.php` |
|
||||
| Entrada XML | `NfeEntradaXmlController.php`, `NfeImportaXmlController.php` |
|
||||
|
||||
**Funcionalidades NF-e:**
|
||||
- Emissão, cancelamento, carta de correção
|
||||
- Importação de XML de entrada
|
||||
- Download de XML/DANFE
|
||||
- Manifesto do destinatário (DFe)
|
||||
- Inutilização de numeração
|
||||
- Contingência offline
|
||||
|
||||
#### NFC-e (Nota Fiscal do Consumidor)
|
||||
| Componente | Arquivo |
|
||||
|-----------|---------|
|
||||
| Controller Web | `NfceController.php` |
|
||||
| Controller API | `API/NFCeController.php` |
|
||||
| Painel API | `API/NFCePainelController.php` |
|
||||
| Service | `NFCeService.php` |
|
||||
| Service API | `NFCeServiceApi.php` |
|
||||
| Model | `Nfce.php`, `ItemNfce.php`, `FaturaNfce.php` |
|
||||
| Views | `nfce/*.blade.php` |
|
||||
| Contingência | `NfceContigenciaController.php` |
|
||||
| Impressão | `ImprimirNfceController.php` |
|
||||
|
||||
#### CT-e (Conhecimento de Transporte)
|
||||
| Componente | Arquivo |
|
||||
|-----------|---------|
|
||||
| Controller Web | `CteController.php` |
|
||||
| Controller API | `API/CTeController.php` |
|
||||
| CT-e OS | `CteOsController.php`, `API/CTeOsPainelController.php` |
|
||||
| Service | `CTeService.php`, `CTeOsService.php` |
|
||||
| Model | `Cte.php`, `MedidaCte.php`, `ComponenteCte.php` |
|
||||
| Views | `cte/*.blade.php` |
|
||||
|
||||
#### MDF-e (Manifesto de Documentos Fiscais)
|
||||
| Componente | Arquivo |
|
||||
|-----------|---------|
|
||||
| Controller Web | `MdfeController.php` |
|
||||
| Controller API | `API/MdfeController.php` |
|
||||
| Service | `MDFeService.php` |
|
||||
| Model | `Mdfe.php`, `Percurso.php`, `UnidadeCarga.php` |
|
||||
| Views | `mdfe/*.blade.php` |
|
||||
|
||||
#### Nota de Serviço (NFS-e)
|
||||
| Componente | Arquivo |
|
||||
|-----------|---------|
|
||||
| Controller Web | `NotaServicoController.php` |
|
||||
| Controller API | `API/NotaServicoController.php` |
|
||||
| Config | `NotaServicoConfigController.php` |
|
||||
| Webhook | `API/NfseWebHookController.php` |
|
||||
|
||||
#### Configurações Fiscais
|
||||
| Componente | Descrição |
|
||||
|-----------|-----------|
|
||||
| `NaturezaOperacaoController.php` | Naturezas de operação (CFOP) |
|
||||
| `NcmController.php` | NCM - Nomenclatura Comum do Mercosul |
|
||||
| `IbptController.php` / `IbptService.php` | IBPT - Carga tributária |
|
||||
| `DifalController.php` | DIFAL - Diferencial de alíquota |
|
||||
| `SpedController.php` / `SpedConfigController.php` | SPED Fiscal |
|
||||
| `SintegraController.php` | SINTEGRA |
|
||||
| `ContigenciaController.php` | Contingência NF-e/NFC-e |
|
||||
| `InutilizacaoController.php` | Inutilização de numeração |
|
||||
|
||||
---
|
||||
|
||||
### 2. VENDAS E PDV
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `VendaController.php` | Vendas completas |
|
||||
| `API/VendaController.php` | API de vendas |
|
||||
| `API/PDV/*.php` | Ponto de Venda (LoginController, ClienteController) |
|
||||
| `API/FrontBoxController.php` | Frontend do caixa |
|
||||
| `FrontBoxController.php` | Interface do caixa Web |
|
||||
| `OrcamentoController.php` | Orçamentos |
|
||||
| `API/PreVendaController.php` | Pré-vendas |
|
||||
| `TrocaController.php` | Devoluções/trocas |
|
||||
| `DevolucaoController.php` | Devoluções formais |
|
||||
| `CupomDescontoController.php` | Cupons de desconto |
|
||||
| `CashBackConfigController.php` | Configuração de cashback |
|
||||
|
||||
---
|
||||
|
||||
### 3. ESTOQUE E PRODUTOS
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `ProdutoController.php` | Cadastro de produtos |
|
||||
| `EstoqueController.php` | Controle de estoque |
|
||||
| `EstoqueLocalizacaoController.php` | Localização no estoque |
|
||||
| `StockEntryController.php` | Entrada de estoque |
|
||||
| `StockEntryService.php` | Serviço de entrada |
|
||||
| `StockSaleService.php` | Serviço de saída por venda |
|
||||
| `TransferenciaEstoqueController.php` | Transferências |
|
||||
| `InventarioController.php` | Inventários |
|
||||
| `VariacaoController.php` | Variações de produto |
|
||||
| `ListaPrecoController.php` | Listas de preço |
|
||||
| `ComboController.php` | Combos/kits |
|
||||
| `UnidadeMedidaController.php` | Unidades de medida |
|
||||
| `MarcaController.php` | Marcas |
|
||||
| `ImportadorController.php` | Importação de produtos |
|
||||
| `ImeiTrackingController.php` | Rastreamento de IMEI |
|
||||
| `API/ImeiUnitController.php` | Unidades IMEI via API |
|
||||
|
||||
---
|
||||
|
||||
### 4. FINANCEIRO
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `ContaPagarController.php` | Contas a pagar |
|
||||
| `ContaReceberController.php` | Contas a receber |
|
||||
| `CaixaController.php` | Controle de caixa |
|
||||
| `SangriaController.php` | Sangrias de caixa |
|
||||
| `SuprimentoController.php` | Suprimentos de caixa |
|
||||
| `BoletoController.php` | Boletos |
|
||||
| `ContaBoletoController.php` | Contas com boleto |
|
||||
| `FinanceiroBoletoController.php` | Financeiro boletos |
|
||||
| `FinanceiroDashboardController.php` | Dashboard financeiro |
|
||||
| `FinanceiroPlanoController.php` | Planos financeiros |
|
||||
| `TaxaCartaoController.php` | Taxas de cartão |
|
||||
| `ApuracaoMensalController.php` | Apuração mensal |
|
||||
| `FechamentoMensalController.php` | Fechamento mensal |
|
||||
| `CategoriaContaController.php` | Categorias contábeis |
|
||||
| `ContaEmpresaController.php` | Contas bancárias |
|
||||
| `AsaasController.php` | Integração Asaas (pagamentos) |
|
||||
| `API/PaymentController.php` | API de pagamentos |
|
||||
|
||||
---
|
||||
|
||||
### 5. CLIENTES E CRM
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `ClienteController.php` | Cadastro de clientes |
|
||||
| `ClienteDeliveryController.php` | Clientes de delivery |
|
||||
| `ClienteScoreController.php` | Score/pontuação de clientes |
|
||||
| `CrmController.php` | CRM completo |
|
||||
| `MensagemPadraoCrmController.php` | Mensagens padrão CRM |
|
||||
| `MensagemCrmLogController.php` | Log de mensagens CRM |
|
||||
| `ConvenioController.php` | Convênios |
|
||||
| `ContratoConfigController.php` | Contratos |
|
||||
| `AssinarContratoController.php` | Assinatura de contratos |
|
||||
| `API/CrmController.php` | API CRM |
|
||||
| `API/EnvioFaturaWppController.php` | Envio de fatura via WhatsApp |
|
||||
|
||||
---
|
||||
|
||||
### 6. FOOD SERVICE (Restaurantes, Pizzarias, Bares)
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| **Cardápio Digital** | |
|
||||
| `ConfigCardapioController.php` | Configuração do cardápio |
|
||||
| `CarrosselCardapioController.php` | Carrossel do cardápio |
|
||||
| `AvaliacaoCardapioController.php` | Avaliações |
|
||||
| `API/Cardapio/*.php` | API do cardápio (carrinho, produtos) |
|
||||
| **Delivery** | |
|
||||
| `API/Delivery/*.php` | API delivery (carrinho, clientes, pedidos, produtos) |
|
||||
| `FuncionamentoDeliveryController.php` | Horários de delivery |
|
||||
| **Comanda** | |
|
||||
| `API/Comanda/*.php` | Sistema de comandas (7 controllers) |
|
||||
| **Atendimento** | |
|
||||
| `AtendimentoController.php` | Atendimentos |
|
||||
| `AtendimentoGarcomController.php` | Atendimento garçom |
|
||||
| `MesaController.php` | Gestão de mesas |
|
||||
| **Pizza** | |
|
||||
| `TamanhoPizzaController.php` | Tamanhos de pizza |
|
||||
| `AdicionalController.php` | Adicionais |
|
||||
| `CategoriaAdicionalController.php` | Categorias de adicionais |
|
||||
| **iFood** | |
|
||||
| `IfoodConfigController.php` | Configuração iFood |
|
||||
| `IfoodCatalogoController.php` | Catálogo iFood |
|
||||
| `IfoodPedidoController.php` | Pedidos iFood |
|
||||
| `IfoodProdutoController.php` | Produtos iFood |
|
||||
|
||||
---
|
||||
|
||||
### 7. SERVIÇOS E ORDENS DE SERVIÇO
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `ServicoController.php` | Cadastro de serviços |
|
||||
| `CategoriaServicoController.php` | Categorias |
|
||||
| `OrdemServicoController.php` | Ordens de serviço |
|
||||
| `API/OrdemServicoController.php` | API de OS |
|
||||
| `AgendamentoController.php` | Agendamentos |
|
||||
| `ConfiguracaoAgendamentoController.php` | Config agendamento |
|
||||
| `FuncionamentoController.php` | Horários |
|
||||
| `InterrupcoesController.php` | Interrupções |
|
||||
| `GarantiaController.php` | Garantias |
|
||||
|
||||
---
|
||||
|
||||
### 8. PRODUÇÃO
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `GestaoProducaoController.php` | Gestão de produção |
|
||||
| `API/GestaoProducaoController.php` | API produção |
|
||||
| `API/OrdemProducaoController.php` | Ordens de produção |
|
||||
| `ApontamentoController.php` | Apontamentos |
|
||||
| `CustoConfiguracaoController.php` | Configuração de custos |
|
||||
| `API/PlanejamentoCustoController.php` | Planejamento de custos |
|
||||
|
||||
---
|
||||
|
||||
### 9. TRANSPORTE E LOGÍSTICA
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `TransportadoraController.php` | Transportadoras |
|
||||
| `FreteController.php` | Fretes |
|
||||
| `TipoDespesaFreteController.php` | Tipos de despesa |
|
||||
| `VeiculoController.php` | Veículos |
|
||||
| `ManutencaoVeiculoController.php` | Manutenção |
|
||||
| `MotoboyController.php` | Motoboys |
|
||||
| `LocalizacaoController.php` | Localização/rastreamento |
|
||||
| `ManifestoController.php` | Manifestos de carga |
|
||||
|
||||
---
|
||||
|
||||
### 10. E-COMMERCE E MARKETPLACE
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| **WooCommerce** | |
|
||||
| `WoocommerceConfigController.php` | Configuração |
|
||||
| `WoocommerceCategoriaController.php` | Categorias |
|
||||
| `WoocommerceProdutoController.php` | Produtos |
|
||||
| `WoocommercePedidoController.php` | Pedidos |
|
||||
| **Mercado Livre** | |
|
||||
| `MercadoLivreAuthController.php` | Autenticação OAuth |
|
||||
| `MercadoLivreConfigController.php` | Configuração |
|
||||
| `MercadoLivreProdutoController.php` | Produtos |
|
||||
| `MercadoLivrePerguntaController.php` | Perguntas |
|
||||
| **NuvemShop** | |
|
||||
| `NuvemShopController.php` | Integração NuvemShop |
|
||||
| **VendiZap** | |
|
||||
| `VendiZapConfigController.php` | Configuração |
|
||||
| `VendiZapProdutoController.php` | Produtos |
|
||||
| `VendiZapPedidoController.php` | Pedidos |
|
||||
| `VendiZapCategoriaController.php` | Categorias |
|
||||
| **Marketplace Próprio** | |
|
||||
| `MarketPlaceConfigController.php` | Configuração |
|
||||
| `DestaqueMarketPlaceController.php` | Destaques |
|
||||
| `ServicoMarketPlaceController.php` | Serviços |
|
||||
| `SegmentoController.php` | Segmentos |
|
||||
| **E-commerce Próprio** | |
|
||||
| `EcommerceConfigController.php` | Configuração |
|
||||
| `API/EcommerceController.php` | API e-commerce |
|
||||
|
||||
---
|
||||
|
||||
### 11. RH E COLABORADORES
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `FuncionarioController.php` | Funcionários |
|
||||
| `FuncionarioEventoController.php` | Eventos de funcionário |
|
||||
| `EventoFuncionarioController.php` | Registro de eventos |
|
||||
| `ComissaoController.php` | Comissões |
|
||||
| `MargemComissaoController.php` | Margens de comissão |
|
||||
| `MetaResultadoController.php` | Metas e resultados |
|
||||
| `ControleAcessoController.php` | Controle de acesso |
|
||||
| `RoleController.php` | Permissões/roles |
|
||||
|
||||
---
|
||||
|
||||
### 12. SEGMENTOS ESPECIALIZADOS
|
||||
|
||||
#### Ótica
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `FormatoArmacaoOticaController.php` | Formatos de armação |
|
||||
| `TipoArmacaoController.php` | Tipos de armação |
|
||||
| `LaboratorioController.php` | Laboratórios óticos |
|
||||
| `TratamentoOticaController.php` | Tratamentos de lente |
|
||||
| `MedicoController.php` | Médicos/oftalmologistas |
|
||||
|
||||
#### Hotelaria
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `AcomodacaoController.php` | Acomodações |
|
||||
| `CategoriaAcomodacaoController.php` | Categorias |
|
||||
| `ReservaController.php` | Reservas |
|
||||
| `ConfigReservaController.php` | Configuração |
|
||||
| `FrigobarController.php` | Frigobar |
|
||||
| `EstacionamentoController.php` | Estacionamento |
|
||||
|
||||
---
|
||||
|
||||
### 13. COMPRAS E FORNECEDORES
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `CompraController.php` | Compras |
|
||||
| `ComprasIAController.php` | Compras com IA |
|
||||
| `FornecedorController.php` | Fornecedores |
|
||||
| `CotacaoController.php` | Cotações |
|
||||
| `CotacaoRespostaController.php` | Respostas de cotação |
|
||||
|
||||
---
|
||||
|
||||
### 14. ADMINISTRAÇÃO MULTI-TENANT
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `EmpresaController.php` | Gestão de empresas |
|
||||
| `UsuarioController.php` | Usuários |
|
||||
| `UsuarioEmissaoController.php` | Usuários de emissão |
|
||||
| `ContadorController.php` | Contador |
|
||||
| `ContadorAdminController.php` | Admin do contador |
|
||||
| `ContadorEmpresaController.php` | Empresas do contador |
|
||||
| `ImpersonateController.php` | Impersonar usuário |
|
||||
| `GerenciarPlanoController.php` | Gestão de planos |
|
||||
| `UpgradePlanoController.php` | Upgrade de planos |
|
||||
| `ConfiguracaoSuperController.php` | Config super admin |
|
||||
| `BackupController.php` | Backups |
|
||||
| `SistemaController.php` | Sistema |
|
||||
| `LogController.php` | Logs |
|
||||
|
||||
---
|
||||
|
||||
### 15. TEF (Transferência Eletrônica de Fundos)
|
||||
|
||||
| Controller | Descrição |
|
||||
|-----------|-----------|
|
||||
| `TefConfigController.php` | Configuração TEF |
|
||||
| `TefController.php` | Operações TEF |
|
||||
| `TefRegistroController.php` | Registros TEF |
|
||||
| Model: `TefMultiPlusCard.php` | Multi Plus Card |
|
||||
|
||||
---
|
||||
|
||||
### 16. INTELIGÊNCIA ARTIFICIAL
|
||||
|
||||
| Componente | Descrição |
|
||||
|-----------|-----------|
|
||||
| `ComprasIAController.php` | Sugestões de compra via IA |
|
||||
| `FinancePrevisaoService.php` | Previsão financeira com IA |
|
||||
|
||||
---
|
||||
|
||||
## Models (320 entidades)
|
||||
|
||||
### Categorias Principais
|
||||
|
||||
| Grupo | Models Exemplo |
|
||||
|-------|---------------|
|
||||
| **Core** | `User`, `Empresa`, `Cidade`, `Config` |
|
||||
| **Produtos** | `Produto`, `ProdutoVariacao`, `ProdutoComposicao`, `ProdutoAdicional`, `Marca`, `CategoriaProduto`, `UnidadeMedida` |
|
||||
| **Vendas** | `Venda`, `ItemVenda`, `Orcamento`, `PreVenda`, `VendaSuspensa`, `Troca`, `Devolucao` |
|
||||
| **Fiscal NF-e** | `Nfe`, `ItemNfe`, `FaturaNfe`, `NaturezaOperacao`, `PadraoTributacaoProduto` |
|
||||
| **Fiscal NFC-e** | `Nfce`, `ItemNfce`, `FaturaNfce`, `Inutilizacao` |
|
||||
| **CT-e** | `Cte`, `MedidaCte`, `ComponenteCte`, `ChaveNfeCte` |
|
||||
| **MDF-e** | `Mdfe`, `Percurso`, `UnidadeCarga`, `ValePedagio`, `Ciot` |
|
||||
| **Financeiro** | `ContaPagar`, `ContaReceber`, `Caixa`, `SangriaCaixa`, `SuprimentoCaixa`, `Pagamento` |
|
||||
| **Estoque** | `Estoque`, `MovimentacaoProduto`, `StockMove`, `TransferenciaEstoque`, `ItemInventario` |
|
||||
| **Pessoas** | `Cliente`, `Fornecedor`, `Funcionario`, `Transportadora`, `Motoboy` |
|
||||
| **Food Service** | `Pedido`, `ItemPedido`, `Adicional`, `TamanhoPizza`, `ConfiguracaoCardapio`, `CarrosselCardapio` |
|
||||
| **CRM** | `CrmAnotacao`, `MensagemPadraoCrm`, `CupomDesconto`, `CashBackConfig` |
|
||||
| **E-commerce** | `WoocommerceConfig`, `WoocommercePedido`, `WoocommerceItemPedido`, `VendiZapConfig` |
|
||||
| **Agendamento** | `Agendamento`, `ItemAgendamento`, `Servico`, `Funcionamento`, `Interrupcao` |
|
||||
| **OS** | `OrdemServico`, `ServicoOs`, `RelatorioOs`, `FuncionarioOs`, `ProdutoOs` |
|
||||
| **Produção** | `OrdemProducao`, `Apontamento`, `CustoConfiguracao` |
|
||||
| **Hotelaria** | `Acomodacao`, `Reserva`, `Frigobar` |
|
||||
| **Ótica** | `OticaOs`, `Laboratorio`, `TratamentoOtica`, `FormatoArmacao`, `TipoArmacao` |
|
||||
|
||||
---
|
||||
|
||||
## Services (Motor de Negócios)
|
||||
|
||||
| Service | Responsabilidade |
|
||||
|---------|-----------------|
|
||||
| `NFeService.php` | Emissão/cancelamento/CC de NF-e via Cloud-DFE SDK |
|
||||
| `NFeServiceApi.php` | API externa de NF-e |
|
||||
| `NFCeService.php` | Emissão de NFC-e (consumidor final) |
|
||||
| `NFCeServiceApi.php` | API externa de NFC-e |
|
||||
| `CTeService.php` | CT-e para transportadoras |
|
||||
| `CTeOsService.php` | CT-e OS (ordem de serviço de transporte) |
|
||||
| `MDFeService.php` | MDF-e para manifestos de carga |
|
||||
| `DFeService.php` | Manifesto do Destinatário |
|
||||
| `StockEntryService.php` | Entrada automática de estoque por NF-e |
|
||||
| `StockSaleService.php` | Baixa automática de estoque por venda |
|
||||
| `IbptService.php` | Consulta IBPT para carga tributária |
|
||||
| `SpeedService.php` | SPED fiscal |
|
||||
| `FinancePrevisaoService.php` | Previsão financeira com IA |
|
||||
|
||||
---
|
||||
|
||||
## Migrations (260 tabelas)
|
||||
|
||||
### Evolução Cronológica
|
||||
|
||||
| Período | Tabelas Criadas | Destaque |
|
||||
|---------|----------------|----------|
|
||||
| 2022 | `users`, `empresas`, `produtos`, `clientes`, `caixas` | Core do sistema |
|
||||
| 2023 Q2 | `nves` (NF-e), `nfces`, `item_nves` | Módulo fiscal |
|
||||
| 2023 Q3 | `pagamentos`, `inutilizacaos` | Financeiro |
|
||||
| 2023 Q4 | `ctes`, `conta_pagars`, `conta_recebers`, `mdves`, `ordem_servicos`, `pedidos` | Transporte, OS, Food |
|
||||
| 2024 Q1 | `cte_os`, `ordem_producaos`, `produto_composicaos` | Produção |
|
||||
| 2024 Q2 | `cotacaos`, `ifood_*`, `woocommerce_*`, `mercado_livre_*` | E-commerce |
|
||||
| 2024 Q3 | `stock_moves`, `imei_units`, `crm_anotacaos` | IMEI, CRM |
|
||||
| 2024 Q4 | `otica_os`, `laboratorios`, `acomodacaos`, `reservas` | Ótica, Hotelaria |
|
||||
| 2025 | `item_inventarios`, `tef_*`, `nuvem_shop_*` | Inventário, TEF, NuvemShop |
|
||||
|
||||
---
|
||||
|
||||
## Docker Infrastructure
|
||||
|
||||
```yaml
|
||||
# plus/docker-compose.yml
|
||||
services:
|
||||
app: # PHP 8.2 FPM
|
||||
build: .
|
||||
volumes: .:/var/www
|
||||
networks: [controlplus]
|
||||
|
||||
web: # Nginx 1.25
|
||||
image: nginx:1.25-alpine
|
||||
ports: "8080:80"
|
||||
depends_on: [app]
|
||||
|
||||
db: # MySQL 8.0
|
||||
image: mysql:8.0
|
||||
volumes: db_data:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: controlplus
|
||||
```
|
||||
|
||||
**Dockerfile:**
|
||||
- Base: `php:8.2-fpm`
|
||||
- Extensions: GD, BCMath, PDO, Zip, Intl, Soap
|
||||
- Node.js 18 (para assets frontend)
|
||||
- Composer para dependências PHP
|
||||
|
||||
---
|
||||
|
||||
## Fluxo de Dados Suite ↔ Plus
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ FLUXO COMPLETO │
|
||||
│ │
|
||||
│ 1. Usuário acessa Arcádia Suite (porta 5000) │
|
||||
│ 2. Navega para módulo Plus (/plus) │
|
||||
│ 3. Suite gera token SSO com dados do usuário │
|
||||
│ 4. Proxy envia requisição para Laravel (porta 8080) │
|
||||
│ 5. Laravel valida SSO, auto-login, exibe interface │
|
||||
│ 6. Operações fiscais (NF-e/NFC-e) executadas no Plus │
|
||||
│ 7. Dados sincronizados via SOE Motor Adapter │
|
||||
│ 8. Suite consome dados para BI, dashboards, governança │
|
||||
│ │
|
||||
│ SUITE (Inteligência) PLUS (Execução) │
|
||||
│ ├── Decide o que vender ├── Emite NF-e/NFC-e │
|
||||
│ ├── Analisa tendências ├── Controla estoque │
|
||||
│ ├── Gera relatórios BI ├── Processa pagamentos │
|
||||
│ ├── Automatiza processos ├── Gerencia caixa │
|
||||
│ └── Governa políticas └── Registra movimentações │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## APIs do Plus
|
||||
|
||||
### Rotas Web (912 rotas) - Agrupadas
|
||||
|
||||
| Grupo | Rotas | Exemplos |
|
||||
|-------|-------|---------|
|
||||
| Dashboard | ~10 | `/home`, `/empresas` |
|
||||
| Produtos | ~60 | `/produtos/*`, `/estoque/*`, `/marcas/*` |
|
||||
| Vendas/PDV | ~50 | `/vendas/*`, `/pdv/*`, `/orcamento/*` |
|
||||
| Fiscal | ~80 | `/nfe/*`, `/nfce/*`, `/cte/*`, `/mdfe/*` |
|
||||
| Financeiro | ~70 | `/conta-pagar/*`, `/conta-receber/*`, `/caixa/*` |
|
||||
| Clientes/CRM | ~40 | `/clientes/*`, `/crm/*` |
|
||||
| Food Service | ~80 | `/cardapio/*`, `/delivery/*`, `/comanda/*`, `/pedidos/*` |
|
||||
| E-commerce | ~60 | `/woocommerce/*`, `/mercado-livre/*`, `/nuvem-shop/*` |
|
||||
| OS/Serviços | ~40 | `/ordem-servico/*`, `/agendamento/*`, `/servicos/*` |
|
||||
| Transporte | ~50 | `/cte/*`, `/mdfe/*`, `/veiculos/*`, `/motoboy/*` |
|
||||
| Admin | ~100 | `/usuarios/*`, `/empresa/*`, `/configuracao/*` |
|
||||
| Contabilidade | ~60 | `/sped/*`, `/apuracao/*`, `/fechamento/*` |
|
||||
| Outros | ~212 | Reservas, ótica, laboratórios, tickets, etc. |
|
||||
|
||||
### Rotas API (634 rotas) - Agrupadas
|
||||
|
||||
| Grupo | Prefixo | Descrição |
|
||||
|-------|---------|-----------|
|
||||
| PDV | `/api/pdv/*` | Ponto de venda mobile |
|
||||
| Cardápio | `/api/cardapio/*` | Cardápio digital |
|
||||
| Delivery | `/api/delivery/*` | Delivery app |
|
||||
| Comanda | `/api/comanda/*` | Sistema de comandas |
|
||||
| Kotlin App | `/api/kotlin/*` | App mobile nativo |
|
||||
| Fiscal | `/api/nfe/*`, `/api/nfce/*` | Emissão fiscal |
|
||||
| CRM | `/api/crm/*` | CRM mobile |
|
||||
| Estoque | `/api/stock-entry/*` | Entrada de estoque |
|
||||
| E-commerce | `/api/woocommerce/*`, `/api/mercadolivre/*` | Integrações |
|
||||
| iFood | `/api/ifood/*` | Integração iFood |
|
||||
|
||||
---
|
||||
|
||||
## Resumo Visual
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ ARCÁDIA PLUS - ERP COMPLETO │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐│
|
||||
│ │ FISCAL │ │ VENDAS │ │ ESTOQUE │ │ FINANCEIRO ││
|
||||
│ │ NF-e/NFC-e │ │ PDV/Caixa │ │ IMEI Track │ │ Contas P/R ││
|
||||
│ │ CT-e/MDF-e │ │ Orçamentos │ │ Inventário │ │ Boletos ││
|
||||
│ │ NFS-e/SPED │ │ Trocas │ │ Transf. │ │ TEF ││
|
||||
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘│
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐│
|
||||
│ │ FOOD │ │ E-COMMERCE │ │ SERVIÇOS │ │ TRANSPORTE ││
|
||||
│ │ Cardápio │ │ WooCommerce│ │ O.S. │ │ CT-e ││
|
||||
│ │ Delivery │ │ Merc.Livre │ │ Agendament │ │ MDF-e ││
|
||||
│ │ iFood │ │ NuvemShop │ │ Garantias │ │ Logística ││
|
||||
│ │ Comandas │ │ VendiZap │ │ │ │ ││
|
||||
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘│
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐│
|
||||
│ │ CRM │ │ RH │ │ PRODUÇÃO │ │ SEGMENTOS ││
|
||||
│ │ Clientes │ │ Comissões │ │ Ordens │ │ Ótica ││
|
||||
│ │ Score │ │ Metas │ │ Apontament │ │ Hotelaria ││
|
||||
│ │ Contratos │ │ Roles │ │ Custos │ │ Marketplace││
|
||||
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘│
|
||||
│ │
|
||||
│ 376 Controllers │ 320 Models │ 12 Services │ 1.194 Views │
|
||||
│ 260 Migrations │ 912 Web Routes │ 634 API Routes │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Arcádia Plus v2.0 - O Motor de Execução do Arcádia Suite*
|
||||
|
|
@ -0,0 +1,469 @@
|
|||
# Arcádia Suite - Mapa Geral do Sistema
|
||||
|
||||
## Visão Geral
|
||||
|
||||
**Arcádia Suite** é o Escritório Estratégico para a Empresa Moderna. Uma plataforma que centraliza produtividade, inteligência, tomada de decisão e governança, orquestrando ERPs, pessoas e dados.
|
||||
|
||||
**Princípio Central:** Separação absoluta entre decisão e execução.
|
||||
- Arcádia **pensa, governa e orienta**
|
||||
- ERPs **executam, registram e obedecem**
|
||||
|
||||
---
|
||||
|
||||
## Arquitetura de 4 Camadas
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ CAMADA DE APRESENTAÇÃO │
|
||||
│ React 18 + TypeScript + Tailwind CSS + shadcn/ui │
|
||||
│ Interface tipo browser com abas + omnibox │
|
||||
│ 66 páginas/módulos │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ CAMADA DE ORQUESTRAÇÃO │
|
||||
│ Express.js + Socket.IO + Manus Agent │
|
||||
│ Porta 5000 (API + WebSocket) │
|
||||
│ 38 arquivos de rotas / 23 ferramentas registradas │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ CAMADA DE INTELIGÊNCIA │
|
||||
│ FastAPI (Contábil 8003, BI 8004, Automação 8005) │
|
||||
│ Communication Engine (Node 8006) │
|
||||
│ OpenAI GPT-4o (Manus/Dev Center) + GPT-4o-mini (WhatsApp) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ CAMADA DE DADOS │
|
||||
│ PostgreSQL + Drizzle ORM │
|
||||
│ Knowledge Graph + ChromaDB (embeddings) │
|
||||
│ Session Store + Multi-tenant │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mapa de Portas
|
||||
|
||||
| Porta | Serviço | Tecnologia |
|
||||
|-------|---------|-----------|
|
||||
| 5000 | API Principal + Frontend | Express.js + React |
|
||||
| 8002 | Motor Fiscal (Fisco) | FastAPI (Python) |
|
||||
| 8003 | Motor Contábil | FastAPI (Python) |
|
||||
| 8004 | Motor BI (Insights) | FastAPI (Python) |
|
||||
| 8005 | Motor Automação | FastAPI (Python) |
|
||||
| 8006 | Motor Comunicação | Node.js/Express |
|
||||
| 8080 | Arcádia Plus (Laravel) | PHP/Laravel |
|
||||
|
||||
---
|
||||
|
||||
## Módulos do Frontend (66 páginas)
|
||||
|
||||
### Núcleo & Administração
|
||||
| Página | Rota | Descrição |
|
||||
|--------|------|-----------|
|
||||
| Home | `/` | Dashboard principal |
|
||||
| SOE | `/soe` | Sistema Operacional Empresarial |
|
||||
| ERP | `/erp` | Módulo ERP legado |
|
||||
| Admin | `/admin` | Administração do sistema |
|
||||
| SuperAdmin | `/super-admin` | Gestão multi-tenant |
|
||||
|
||||
### Módulos de Negócio
|
||||
| Página | Rota | Descrição |
|
||||
|--------|------|-----------|
|
||||
| Financeiro | `/financeiro` | Contas a pagar/receber, fluxo de caixa |
|
||||
| Contábil | `/contabil` | Contabilidade, DRE, balancetes |
|
||||
| Fiscal | `/fisco` | NF-e, NFC-e, CFOP, NCM, CEST |
|
||||
| CRM | `/crm` | Gestão de relacionamento com cliente |
|
||||
| People | `/people` | RH, colaboradores, folha |
|
||||
| Production | `/production` | Ordens de produção |
|
||||
| Quality | `/quality` | Controle de qualidade |
|
||||
|
||||
### Varejo & Comércio
|
||||
| Página | Rota | Descrição |
|
||||
|--------|------|-----------|
|
||||
| Retail | `/retail` | Varejo (celulares, assistência técnica) |
|
||||
| RetailReports | `/retail-reports` | Relatórios do varejo |
|
||||
| Marketplace | `/marketplace` | Marketplace integrado |
|
||||
| Valuation | `/valuation` | Avaliação de trade-in |
|
||||
|
||||
### Comunicação
|
||||
| Página | Rota | Descrição |
|
||||
|--------|------|-----------|
|
||||
| WhatsApp | `/whatsapp` | Multi-sessão WhatsApp |
|
||||
| Chat | `/chat` | Chat interno |
|
||||
| XOS Inbox | `/xos/inbox` | Caixa de entrada unificada |
|
||||
| XOS CRM | `/xos/crm` | CRM unificado |
|
||||
| XOS Campaigns | `/xos/campaigns` | Campanhas de marketing |
|
||||
| XOS Tickets | `/xos/tickets` | Sistema de tickets |
|
||||
|
||||
### Inteligência & IA
|
||||
| Página | Rota | Descrição |
|
||||
|--------|------|-----------|
|
||||
| Scientist | `/scientist` | Auto-programação com IA |
|
||||
| Knowledge | `/knowledge` | Base de conhecimento/grafo |
|
||||
| BI Workspace | `/bi` | Business Intelligence |
|
||||
| Manus | `/agent` | Agente autônomo central |
|
||||
|
||||
### Desenvolvimento & DevOps
|
||||
| Página | Rota | Descrição |
|
||||
|--------|------|-----------|
|
||||
| IDE | `/ide` | Editor Monaco + Terminal |
|
||||
| Dev Center | `/dev-center` | Centro de desenvolvimento XOS |
|
||||
| XOS Pipeline | `/xos/pipeline` | Pipeline autônomo de código |
|
||||
| XOS Governance | `/xos/governance` | Governança e políticas |
|
||||
| API Hub | `/api-hub` | Central de APIs |
|
||||
| API Tester | `/api-tester` | Testador de APIs |
|
||||
| DocType Builder | `/doctype-builder` | Construtor de tipos |
|
||||
| Page Builder | `/page-builder` | Construtor de páginas |
|
||||
| Workflow Builder | `/workflow-builder` | Construtor de workflows |
|
||||
|
||||
### Operações & Engenharia
|
||||
| Página | Rota | Descrição |
|
||||
|--------|------|-----------|
|
||||
| Engineering Hub | `/engineering` | Hub de engenharia |
|
||||
| Field Operations | `/field-ops` | Operações de campo |
|
||||
| Process Compass | `/compass` | Bússola de processos |
|
||||
| Suppliers Portal | `/suppliers` | Portal de fornecedores |
|
||||
|
||||
### Plataforma
|
||||
| Página | Rota | Descrição |
|
||||
|--------|------|-----------|
|
||||
| Engine Room | `/engine-room` | Casa de Máquinas (status dos motores) |
|
||||
| Automations | `/automations` | Motor de automações |
|
||||
| Plus | `/plus` | ERP Laravel (proxy) |
|
||||
| LMS | `/lms` | Sistema de aprendizagem |
|
||||
| Communities | `/communities` | Comunidades |
|
||||
| Support | `/support` | Central de suporte |
|
||||
| Migration | `/migration` | Migração de dados |
|
||||
| Central APIs | `/central-apis` | APIs centrais |
|
||||
|
||||
---
|
||||
|
||||
## APIs do Backend (38 grupos de rotas)
|
||||
|
||||
### Core
|
||||
| Rota Base | Arquivo | Descrição |
|
||||
|-----------|---------|-----------|
|
||||
| `/api/login`, `/api/register` | `server/auth.ts` | Autenticação |
|
||||
| `/api/admin/*` | `server/admin/routes.ts` | Administração |
|
||||
| `/api/erp/*` | `server/erp/routes.ts` | ERP principal |
|
||||
| `/api/soe/*` | `server/erp/routes.ts` | SOE (alias) |
|
||||
| `/api/users/*` | `server/routes.ts` | Gestão de usuários |
|
||||
|
||||
### Negócio
|
||||
| Rota Base | Arquivo | Descrição |
|
||||
|-----------|---------|-----------|
|
||||
| `/api/financeiro/*` | `server/financeiro/routes.ts` | Financeiro |
|
||||
| `/api/contabil/*` | `server/contabil/routes.ts` | Contabilidade |
|
||||
| `/api/fisco/*` | `server/fisco/routes.ts` | Fiscal |
|
||||
| `/api/crm/*` | `server/crm/routes.ts` | CRM |
|
||||
| `/api/people/*` | `server/people/routes.ts` | RH/Pessoas |
|
||||
| `/api/production/*` | `server/production/routes.ts` | Produção |
|
||||
| `/api/quality/*` | `server/quality/routes.ts` | Qualidade |
|
||||
| `/api/retail/*` | `server/retail/routes.ts` | Varejo |
|
||||
| `/api/valuation/*` | `server/valuation/routes.ts` | Avaliação trade-in |
|
||||
| `/api/marketplace/*` | `server/marketplace/routes.ts` | Marketplace |
|
||||
|
||||
### Comunicação
|
||||
| Rota Base | Arquivo | Descrição |
|
||||
|-----------|---------|-----------|
|
||||
| `/api/whatsapp/*` | `server/whatsapp/routes.ts` | WhatsApp multi-sessão |
|
||||
| `/api/chat/*` | `server/chat/routes.ts` | Chat interno |
|
||||
| `/api/email/*` | `server/email/routes.ts` | E-mail |
|
||||
| `/api/comm/*` | proxy | Motor de Comunicação |
|
||||
| `/api/xos/*` | `server/xos/routes.ts` | XOS CRM unificado |
|
||||
|
||||
### Inteligência
|
||||
| Rota Base | Arquivo | Descrição |
|
||||
|-----------|---------|-----------|
|
||||
| `/api/manus/*` | `server/manus/routes.ts` | Agente Manus IA |
|
||||
| `/api/knowledge/*` | `server/learning/routes.ts` | Knowledge Graph |
|
||||
| `/api/bi/*` | `server/bi/routes.ts` | Business Intelligence |
|
||||
| `/api/bi/metaset/*` | `server/metaset/routes.ts` | Motor BI MetaSet |
|
||||
| `/api/scientist/*` | `server/routes.ts` | Cientista de dados |
|
||||
|
||||
### Desenvolvimento
|
||||
| Rota Base | Arquivo | Descrição |
|
||||
|-----------|---------|-----------|
|
||||
| `/api/ide/*` | `server/ide/routes.ts` | IDE integrada |
|
||||
| `/api/dev-center/*` | `server/blackboard/routes.ts` | Dev Center/Blackboard |
|
||||
| `/api/xos/pipeline` | `server/blackboard/routes.ts` | Pipeline autônomo |
|
||||
| `/api/governance/*` | `server/governance/routes.ts` | Governança |
|
||||
| `/api/lowcode/*` | `server/lowcode/routes.ts` | Low-code engine |
|
||||
|
||||
### Protocolos de Interoperabilidade
|
||||
| Rota Base | Arquivo | Descrição |
|
||||
|-----------|---------|-----------|
|
||||
| `/api/mcp/v1/*` | `server/mcp/routes.ts` | Model Context Protocol |
|
||||
| `/api/a2a/v1/*` | `server/routes.ts` | Agent to Agent Protocol |
|
||||
| `/api/api-central/*` | `server/api-central/routes.ts` | Central de APIs |
|
||||
|
||||
### Infraestrutura
|
||||
| Rota Base | Arquivo | Descrição |
|
||||
|-----------|---------|-----------|
|
||||
| `/api/engine-room/*` | `server/engine-room/routes.ts` | Casa de Máquinas |
|
||||
| `/api/automations/*` | `server/automations/routes.ts` | Motor de Automação |
|
||||
| `/api/modules/*` | `server/modules/loader.ts` | Módulos dinâmicos |
|
||||
| `/api/login-bridge/*` | `server/login-bridge/routes.ts` | SSO Bridge |
|
||||
| `/api/migration/*` | `server/migration/routes.ts` | Migração |
|
||||
|
||||
---
|
||||
|
||||
## Motores (Engines)
|
||||
|
||||
### Motor IA - Manus (Node.js, porta 5000)
|
||||
- **Modelo:** GPT-4o (Dev Center), GPT-4o-mini (WhatsApp)
|
||||
- **Agentes:** 6 agentes autônomos (Architect, Generator, Validator, Executor, Researcher, Evolution)
|
||||
- **Ferramentas:** 23 ferramentas registradas (GitHub, filesystem, BI, git)
|
||||
- **Pipeline:** Design → Codegen → Validation → Staging → Evolution
|
||||
|
||||
### Motor Fiscal - Fisco (Python, porta 8002)
|
||||
- NF-e / NFC-e via nfelib
|
||||
- NCMs, CFOPs, CESTs, grupos tributários
|
||||
- Certificados digitais
|
||||
- Comunicação com SEFAZ
|
||||
|
||||
### Motor Contábil (Python, porta 8003)
|
||||
- Plano de contas
|
||||
- Lançamentos contábeis
|
||||
- DRE, Balanço Patrimonial
|
||||
|
||||
### Motor BI - Insights (Python, porta 8004)
|
||||
- Execução SQL
|
||||
- Geração de gráficos
|
||||
- Análise com Pandas
|
||||
- Cache inteligente
|
||||
|
||||
### Motor Automação (Python, porta 8005)
|
||||
- Cron scheduler
|
||||
- Event bus
|
||||
- Executor de workflows
|
||||
|
||||
### Motor Comunicação (Node.js, porta 8006)
|
||||
- Unifica XOS CRM + WhatsApp + Email
|
||||
- Contatos, threads, mensagens unificados
|
||||
- Filas de atendimento
|
||||
- Eventos para Knowledge Graph
|
||||
|
||||
### Arcádia Plus - ERP Laravel (PHP, porta 8080)
|
||||
- NF-e/NFC-e/CT-e/MDF-e
|
||||
- PDV (ponto de venda)
|
||||
- Cardápio digital
|
||||
- Ordens de serviço
|
||||
- Estoque com rastreabilidade
|
||||
- Integrações e-commerce (WooCommerce, Mercado Livre, NuvemShop)
|
||||
- Integrações delivery (iFood)
|
||||
|
||||
---
|
||||
|
||||
## Dev Center XOS - 6 Agentes Autônomos
|
||||
|
||||
```
|
||||
Prompt em Português
|
||||
│
|
||||
▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Architect │ ──▶ │ Generator │ ──▶ │ Validator │
|
||||
│ (Design) │ │ (Codegen) │ │ (Typecheck) │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Evolution │ ◀── │ Researcher │ ◀── │ Executor │
|
||||
│ (Aprende) │ │ (Pesquisa) │ │ (Staging) │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Módulo Retail (Varejo de Celulares)
|
||||
|
||||
### Funcionalidades Core
|
||||
- **Vendas com IMEI:** Rastreamento individual de aparelhos
|
||||
- **Trade-in:** Avaliação com checklist de 19 itens
|
||||
- **Ordens de Serviço:** Gestão completa de assistência técnica
|
||||
- **Garantia:** Controle de garantias por IMEI
|
||||
- **Caixa Diário:** Reconciliação de caixa com fechamento
|
||||
- **Comissões:** Cálculo automático por vendedor
|
||||
|
||||
### Checklist Trade-in (19 itens)
|
||||
1. Liga normalmente
|
||||
2. Problemas na tela
|
||||
3. WiFi funcionando
|
||||
4. Bluetooth funcionando
|
||||
5. Câmera frontal
|
||||
6. Câmera traseira
|
||||
7. Microfone
|
||||
8. Alto-falante
|
||||
9. Botões físicos
|
||||
10. Sensor biométrico
|
||||
11. Carregamento
|
||||
12. Bateria saudável
|
||||
13. GPS funcionando
|
||||
14. Giroscópio
|
||||
15. Acelerômetro
|
||||
16. NFC
|
||||
17. Resistência à água
|
||||
18. Face ID / reconhecimento facial
|
||||
19. Vibração
|
||||
|
||||
---
|
||||
|
||||
## Banco de Dados (PostgreSQL + Drizzle ORM)
|
||||
|
||||
### Tabelas Principais
|
||||
| Grupo | Tabelas |
|
||||
|-------|---------|
|
||||
| **Identidade** | `users`, `profiles`, `tenants` |
|
||||
| **Produtividade** | `workspace_pages`, `page_blocks`, `dashboard_widgets`, `quick_notes` |
|
||||
| **Comunicação** | `conversations`, `messages`, `chat_threads`, `chat_messages` |
|
||||
| **WhatsApp** | `whatsapp_sessions`, `whatsapp_contacts`, `whatsapp_messages`, `whatsapp_tickets` |
|
||||
| **ERP Core** | `applications`, `erp_connections`, `agent_tasks`, `task_executions` |
|
||||
| **Conhecimento** | `knowledge_base`, `knowledge_graph_nodes`, `knowledge_graph_edges` |
|
||||
| **Governança** | `xos_governance_*`, `xos_job_queue`, `xos_agent_metrics` |
|
||||
| **Pipeline** | `xos_staging_changes`, `xos_dev_pipelines` |
|
||||
| **Comunicação Unificada** | `comm_contacts`, `comm_threads`, `comm_messages`, `comm_channels` |
|
||||
| **Varejo** | Via módulos dinâmicos (`/api/modules/retail-reports`) |
|
||||
| **Financeiro** | Contas, lançamentos, conciliação |
|
||||
| **Fiscal** | NCMs, CFOPs, notas fiscais |
|
||||
|
||||
---
|
||||
|
||||
## Integrações Externas
|
||||
|
||||
| Serviço | Uso |
|
||||
|---------|-----|
|
||||
| **OpenAI** | GPT-4o (Manus, Dev Center), GPT-4o-mini (WhatsApp) |
|
||||
| **GitHub** | Commits automáticos, análise de repositórios |
|
||||
| **ERPNext** | Integração com ERP externo (clientes, produtos, vendas) |
|
||||
| **WhatsApp/Baileys** | Multi-sessão de atendimento |
|
||||
| **SEFAZ** | NF-e/NFC-e via nfelib |
|
||||
| **Cloud-DFE** | SDK fiscal (NF-e, NFC-e, CT-e, MDF-e) |
|
||||
| **WooCommerce** | E-commerce integration |
|
||||
| **Mercado Livre** | Marketplace |
|
||||
| **NuvemShop** | E-commerce |
|
||||
| **iFood** | Delivery (pedidos, cardápio) |
|
||||
| **Asaas** | Pagamentos, boletos |
|
||||
|
||||
---
|
||||
|
||||
## Protocolos de Interoperabilidade
|
||||
|
||||
| Protocolo | Rota | Descrição |
|
||||
|-----------|------|-----------|
|
||||
| **MCP** | `/api/mcp/v1/` | Model Context Protocol - exposição de ferramentas |
|
||||
| **A2A** | `/api/a2a/v1/` | Agent to Agent - comunicação bidirecional |
|
||||
| **AP2** | Planejado | Agent Payment Protocol |
|
||||
| **UCP** | Planejado | Unified Commerce Protocol |
|
||||
|
||||
---
|
||||
|
||||
## Como Rodar Localmente
|
||||
|
||||
### Pré-requisitos
|
||||
- Node.js 20+
|
||||
- Python 3.11+
|
||||
- PostgreSQL 16+
|
||||
- PHP 8.2+ (opcional, para Arcádia Plus)
|
||||
|
||||
### Instalação
|
||||
|
||||
```bash
|
||||
# 1. Extrair o backup
|
||||
tar xzf arcadia-suite-backup.tar.gz
|
||||
|
||||
# 2. Instalar dependências Node
|
||||
npm install
|
||||
|
||||
# 3. Instalar dependências Python
|
||||
pip install fastapi uvicorn pandas numpy psycopg2-binary nfelib lxml cryptography
|
||||
|
||||
# 4. Configurar variáveis de ambiente
|
||||
cp .env.example .env
|
||||
# Editar .env com suas credenciais:
|
||||
# DATABASE_URL=postgresql://user:pass@localhost:5432/arcadia
|
||||
# OPENAI_API_KEY=sk-...
|
||||
# GITHUB_TOKEN=ghp_...
|
||||
|
||||
# 5. Criar banco de dados
|
||||
createdb arcadia
|
||||
|
||||
# 6. Executar migrations
|
||||
npx drizzle-kit push
|
||||
|
||||
# 7. Iniciar em desenvolvimento
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Variáveis de Ambiente Necessárias
|
||||
| Variável | Descrição |
|
||||
|----------|-----------|
|
||||
| `DATABASE_URL` | URL de conexão PostgreSQL |
|
||||
| `OPENAI_API_KEY` | Chave da API OpenAI |
|
||||
| `GITHUB_TOKEN` | Token GitHub para integrações |
|
||||
| `ERPNEXT_URL` | URL do ERPNext (opcional) |
|
||||
| `ERPNEXT_API_KEY` | Chave API ERPNext (opcional) |
|
||||
| `ERPNEXT_API_SECRET` | Segredo API ERPNext (opcional) |
|
||||
| `SESSION_SECRET` | Segredo para sessões Express |
|
||||
|
||||
### Credenciais Padrão
|
||||
- **Usuário:** admin
|
||||
- **Senha:** admin
|
||||
- **Role:** master
|
||||
|
||||
---
|
||||
|
||||
## Estrutura de Diretórios
|
||||
|
||||
```
|
||||
arcadia-suite/
|
||||
├── client/ # Frontend React
|
||||
│ ├── src/
|
||||
│ │ ├── pages/ # 66 páginas
|
||||
│ │ ├── components/ # Componentes reutilizáveis
|
||||
│ │ ├── hooks/ # Custom hooks
|
||||
│ │ └── lib/ # Utilitários
|
||||
│ └── public/ # Assets estáticos
|
||||
├── server/ # Backend Express
|
||||
│ ├── admin/ # Administração
|
||||
│ ├── autonomous/ # Ferramentas autônomas
|
||||
│ ├── bi/ # Business Intelligence
|
||||
│ ├── blackboard/ # Dev Center (6 agentes)
|
||||
│ ├── chat/ # Chat interno
|
||||
│ ├── communication/ # Motor de comunicação
|
||||
│ ├── contabil/ # Motor contábil
|
||||
│ ├── crm/ # CRM
|
||||
│ ├── engine-room/ # Casa de Máquinas
|
||||
│ ├── erp/ # ERP/SOE
|
||||
│ ├── financeiro/ # Financeiro
|
||||
│ ├── fisco/ # Fiscal
|
||||
│ ├── governance/ # Governança XOS
|
||||
│ ├── ide/ # IDE integrada
|
||||
│ ├── integrations/ # Integrações externas
|
||||
│ ├── learning/ # Knowledge Graph
|
||||
│ ├── manus/ # Agente Manus
|
||||
│ ├── mcp/ # Model Context Protocol
|
||||
│ ├── modules/ # Módulos dinâmicos
|
||||
│ ├── people/ # RH
|
||||
│ ├── plus/ # Proxy Laravel
|
||||
│ ├── production/ # Produção
|
||||
│ ├── python/ # Scripts Python
|
||||
│ ├── quality/ # Qualidade
|
||||
│ ├── retail/ # Varejo
|
||||
│ ├── whatsapp/ # WhatsApp
|
||||
│ └── xos/ # XOS unificado
|
||||
├── shared/ # Código compartilhado
|
||||
│ ├── schema.ts # Schema principal (Drizzle)
|
||||
│ └── schemas/ # Schemas modulares
|
||||
├── plus/ # ERP Laravel (PHP)
|
||||
├── python-service/ # Serviço Python
|
||||
├── db/ # Configuração do banco
|
||||
├── migrations/ # Migrations Drizzle
|
||||
└── docs/ # Documentação
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Arcádia Suite v2.0 - O Escritório Estratégico para a Empresa Moderna*
|
||||
|
|
@ -0,0 +1,787 @@
|
|||
# PLANO BI — Substituição Metabase → Apache Superset
|
||||
## Análise Real + Plano de Migração
|
||||
### Versão 1.0 — Março 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. DIAGNÓSTICO — ESTADO ATUAL
|
||||
|
||||
### 1.1 O que foi descoberto no código
|
||||
|
||||
O documento do Replit referenciava **Metabase** (Java, :8088) como o pilar visual do BI. Ao analisar o código real, a situação é diferente:
|
||||
|
||||
| Componente | Nome no doc | Nome real no código | Status |
|
||||
|---|---|---|---|
|
||||
| Plataforma BI Visual | "Metabase" | **"MetaSet"** (alias) | Metabase por baixo |
|
||||
| Proxy | `/metabase/*` | `server/metabase/proxy.ts` | Funcional |
|
||||
| Auth | Metabase session | `server/metaset/routes.ts` (autologin) | Funcional |
|
||||
| Client | MetabaseClient | `server/metaset/client.ts` | Funcional |
|
||||
| UI | Advanced tab iframe | `/api/bi/metaset/autologin` → `/metabase/` | Funcional |
|
||||
| **Superset** | Não mencionado | **`docker-compose.yml` linha 165** | **JÁ CONFIGURADO** |
|
||||
|
||||
**Descoberta crítica:** O Apache Superset **já está no docker-compose.yml**:
|
||||
```yaml
|
||||
superset:
|
||||
image: apache/superset:4.1.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
|
||||
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia_superset
|
||||
ports:
|
||||
- "8088:8088"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
profiles: [bi]
|
||||
```
|
||||
|
||||
O Superset está configurado, mas **não integrado ao gateway Node.js**. O que falta é a ponte.
|
||||
|
||||
### 1.2 Arquivos que precisam mudar vs o que fica igual
|
||||
|
||||
**Mudam:**
|
||||
```
|
||||
server/metabase/proxy.ts → substituir por server/superset/proxy.ts
|
||||
server/metaset/routes.ts → substituir por server/superset/routes.ts
|
||||
server/metaset/client.ts → substituir por server/superset/client.ts
|
||||
client/src/pages/BiWorkspace.tsx (Advanced tab apenas)
|
||||
docker-compose.yml (Superset config + init)
|
||||
docker-compose.prod.yml (adicionar Superset)
|
||||
.env.example (novas variáveis)
|
||||
```
|
||||
|
||||
**NÃO mudam (ficam exatamente iguais):**
|
||||
```
|
||||
server/python/bi_engine.py ← Motor Python :8004 continua intacto
|
||||
server/bi/routes.ts ← CRUD de datasets/charts/dashboards
|
||||
server/bi/upload.ts ← ETL pipeline
|
||||
server/bi/staging.ts ← Staging
|
||||
server/bi/engine-proxy.ts ← Proxy para BI Engine Python
|
||||
client/src/pages/BiWorkspace.tsx (7 de 8 tabs ficam iguais)
|
||||
Todas as tabelas do banco (bi_datasets, bi_charts, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. POR QUE SUPERSET É MELHOR PARA O CASO
|
||||
|
||||
### 2.1 Metabase vs Superset
|
||||
|
||||
| Critério | Metabase | Apache Superset |
|
||||
|---|---|---|
|
||||
| Runtime | Java (JVM, 500MB+ RAM) | Python/Flask (mais leve) |
|
||||
| Embedding | Iframe com cookie de sessão | **Guest Token JWT** (seguro, sem credenciais) |
|
||||
| "Qualquer tela" | Iframe estático, limitado | **`@superset-ui/embedded-sdk`** — componente React nativo |
|
||||
| REST API | Limitada e não documentada | **REST API v1 completa e documentada** |
|
||||
| Consistência de stack | Java (fora do padrão) | Python (mesmo stack dos motores existentes) |
|
||||
| Docker image | `metabase/metabase` ~700MB | `apache/superset` ~400MB |
|
||||
| SQL Lab | Básico | **Poderoso** (autocomplete, histórico, salvar queries) |
|
||||
| Alertas | Básico | **Avançado** (webhooks, Slack, email) |
|
||||
| Tipos de chart | ~20 | **50+** (incluindo mapas, sunburst, funnel) |
|
||||
| Banco de metadados | H2/Postgres separado | **Mesmo PostgreSQL** da Arcádia |
|
||||
|
||||
### 2.2 A feature que o usuário pediu: "funcionar em qualquer tela"
|
||||
|
||||
O Superset resolve isso com **Guest Token + Embedded SDK**:
|
||||
|
||||
```
|
||||
Fluxo atual (Metabase):
|
||||
BiWorkspace → iframe → /metabase/ → autologin com cookie
|
||||
❌ Só funciona na tab Advanced do BiWorkspace
|
||||
❌ Cookie expira, precisa re-autenticar
|
||||
❌ Não embeda em outras páginas facilmente
|
||||
|
||||
Fluxo novo (Superset):
|
||||
QUALQUER página React → <EmbeddedDashboard dashboardId="xxx" />
|
||||
✅ Funciona no Dashboard principal (/)
|
||||
✅ Funciona no SOE (/soe tab Financeiro)
|
||||
✅ Funciona no /contabil
|
||||
✅ Funciona no /financeiro
|
||||
✅ Funciona em qualquer tela do Arcádia
|
||||
✅ Guest Token renovado automaticamente (sem re-login)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. ARQUITETURA COM SUPERSET
|
||||
|
||||
### 3.1 Diagrama atualizado
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ USUÁRIO / FRONTEND │
|
||||
│ │
|
||||
│ BiWorkspace.tsx (8 tabs — 7 iguais, Advanced → Superset) │
|
||||
│ + QUALQUER outra página pode embedar um dashboard Superset │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ <SupersetDashboard dashboardId="finance-overview" /> │ │
|
||||
│ │ Componente React reutilizável em QUALQUER rota │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────┬────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼──────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌────────────────┐ ┌──────────────────────┐
|
||||
│ API BI Node │ │ BI Engine Py │ │ Superset (Python) │
|
||||
│ /api/bi/* │ │ /api/bi-engine │ │ /superset/* │
|
||||
│ (sem mudança)│ │ :8004 (igual) │ │ :8088 │
|
||||
│ │ │ │ │ │
|
||||
│ + novo: │ │ │ │ REST API v1 │
|
||||
│ /api/superset│ │ │ │ Guest Token JWT │
|
||||
│ /guest-token │ │ │ │ SQL Lab │
|
||||
│ │ │ │ │ 50+ chart types │
|
||||
└──────────────┘ └────────────────┘ └──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ PostgreSQL │
|
||||
│ arcadia (dados SOE) │
|
||||
│ arcadia_superset │
|
||||
│ (metadados Superset) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Dois bancos PostgreSQL
|
||||
|
||||
```
|
||||
Banco: arcadia → Dados do SOE (persons, products, sales_orders, etc.)
|
||||
Superset conecta aqui em READ-ONLY para criar charts
|
||||
Banco: arcadia_superset → Metadados do Superset (dashboards, queries, users)
|
||||
Superset usa internamente
|
||||
```
|
||||
|
||||
Ambos no mesmo servidor PostgreSQL, bancos separados. O docker-compose.yml já referencia `arcadia_superset`.
|
||||
|
||||
---
|
||||
|
||||
## 4. O QUE CONSTRUIR — Passo a Passo
|
||||
|
||||
### 4.1 Configuração do Superset (docker-compose.yml)
|
||||
|
||||
O container já existe. Faltam: init script, volumes, configuração de embedding.
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml — seção superset (expandida)
|
||||
|
||||
superset:
|
||||
image: apache/superset:4.1.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia_superset
|
||||
SUPERSET_WEBSERVER_PORT: 8088
|
||||
PYTHONPATH: /app/pythonpath
|
||||
# Para embedding funcionar:
|
||||
FEATURE_FLAGS: '{"EMBEDDED_SUPERSET": true, "ENABLE_TEMPLATE_PROCESSING": true}'
|
||||
ports:
|
||||
- "8088:8088"
|
||||
volumes:
|
||||
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py
|
||||
- ./docker/superset/init.sh:/app/docker/init.sh
|
||||
- superset_home:/app/superset_home
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
command: ["/bin/bash", "/app/docker/init.sh"]
|
||||
profiles: [bi]
|
||||
networks:
|
||||
- arcadia
|
||||
```
|
||||
|
||||
### 4.2 Arquivo de Configuração Superset
|
||||
|
||||
```python
|
||||
# docker/superset/superset_config.py
|
||||
|
||||
import os
|
||||
|
||||
# Chave secreta (mesma do .env)
|
||||
SECRET_KEY = os.environ.get("SUPERSET_SECRET_KEY", "change-in-production")
|
||||
|
||||
# Banco de metadados do Superset
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||
"DATABASE_URL",
|
||||
"postgresql://arcadia:arcadia123@db:5432/arcadia_superset"
|
||||
)
|
||||
|
||||
# CORS — permite o gateway Arcádia (:5000) chamar a API
|
||||
ENABLE_CORS = True
|
||||
CORS_OPTIONS = {
|
||||
"supports_credentials": True,
|
||||
"allow_headers": ["*"],
|
||||
"resources": {r"/api/*": {"origins": "*"}},
|
||||
}
|
||||
|
||||
# Embedding habilitado
|
||||
FEATURE_FLAGS = {
|
||||
"EMBEDDED_SUPERSET": True,
|
||||
"ENABLE_TEMPLATE_PROCESSING": True,
|
||||
"ALERT_REPORTS": True, # Alertas automáticos
|
||||
"DRILL_TO_DETAIL": True, # Drill-down em charts
|
||||
}
|
||||
|
||||
# Timeout de queries (30s por default, aumentar para análises pesadas)
|
||||
SUPERSET_WEBSERVER_TIMEOUT = 300
|
||||
|
||||
# Cache (Redis se disponível, senão in-memory)
|
||||
CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "SimpleCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 300, # 5 min
|
||||
}
|
||||
|
||||
# Tema/branding Arcádia (logo, cores)
|
||||
APP_NAME = "Arcádia Insights"
|
||||
APP_ICON = "/static/arcadia-logo.png"
|
||||
```
|
||||
|
||||
### 4.3 Init Script do Superset
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# docker/superset/init.sh — roda ao iniciar o container
|
||||
|
||||
set -e
|
||||
|
||||
echo "[Superset Init] Iniciando configuração..."
|
||||
|
||||
# Aguarda PostgreSQL
|
||||
until psql "${DATABASE_URL}" -c "SELECT 1" > /dev/null 2>&1; do
|
||||
echo "[Superset Init] Aguardando PostgreSQL..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Cria banco arcadia_superset se não existir
|
||||
psql "postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/postgres" \
|
||||
-c "CREATE DATABASE arcadia_superset OWNER arcadia;" 2>/dev/null || true
|
||||
|
||||
# Inicializa banco do Superset
|
||||
superset db upgrade
|
||||
|
||||
# Cria usuário admin (só se não existir)
|
||||
superset fab create-admin \
|
||||
--username "${SUPERSET_ADMIN_USER:-admin}" \
|
||||
--firstname "Arcádia" \
|
||||
--lastname "Admin" \
|
||||
--email "${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}" \
|
||||
--password "${SUPERSET_ADMIN_PASSWORD:-arcadia2026}" 2>/dev/null || true
|
||||
|
||||
# Carrega exemplos (opcional, comentar em prod)
|
||||
# superset load_examples
|
||||
|
||||
# Inicializa roles e permissões
|
||||
superset init
|
||||
|
||||
# Registra conexão com o banco Arcádia (dados do SOE)
|
||||
python - <<'PYEOF'
|
||||
from superset import create_app
|
||||
from superset.extensions import db
|
||||
from superset.models.core import Database
|
||||
import os
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
existing = db.session.query(Database).filter_by(
|
||||
database_name="Arcádia Suite"
|
||||
).first()
|
||||
if not existing:
|
||||
arcadia_db = Database(
|
||||
database_name="Arcádia Suite",
|
||||
sqlalchemy_uri=os.environ.get(
|
||||
"ARCADIA_DATABASE_URL",
|
||||
"postgresql://arcadia:arcadia123@db:5432/arcadia"
|
||||
),
|
||||
expose_in_sqllab=True,
|
||||
allow_run_async=True,
|
||||
allow_csv_upload=False,
|
||||
)
|
||||
db.session.add(arcadia_db)
|
||||
db.session.commit()
|
||||
print("[Superset Init] Banco 'Arcádia Suite' registrado!")
|
||||
else:
|
||||
print("[Superset Init] Banco 'Arcádia Suite' já existe.")
|
||||
PYEOF
|
||||
|
||||
echo "[Superset Init] Iniciando servidor..."
|
||||
gunicorn \
|
||||
--bind "0.0.0.0:${SUPERSET_WEBSERVER_PORT:-8088}" \
|
||||
--access-logfile "-" \
|
||||
--error-logfile "-" \
|
||||
--workers 4 \
|
||||
--timeout 120 \
|
||||
--limit-request-line 0 \
|
||||
--limit-request-field_size 0 \
|
||||
"superset.app:create_app()"
|
||||
```
|
||||
|
||||
### 4.4 Proxy Superset (substitui server/metabase/proxy.ts)
|
||||
|
||||
```typescript
|
||||
// server/superset/proxy.ts
|
||||
|
||||
import { Express, Response } from "express";
|
||||
import { createProxyMiddleware } from "http-proxy-middleware";
|
||||
|
||||
const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost";
|
||||
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
|
||||
const SUPERSET_TIMEOUT = 60000;
|
||||
|
||||
export function setupSupersetProxy(app: Express): void {
|
||||
const target = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
|
||||
|
||||
const supersetProxy = createProxyMiddleware({
|
||||
target,
|
||||
changeOrigin: true,
|
||||
timeout: SUPERSET_TIMEOUT,
|
||||
proxyTimeout: SUPERSET_TIMEOUT,
|
||||
pathRewrite: { "^/superset": "" },
|
||||
on: {
|
||||
error: (err, _req, res) => {
|
||||
console.error("[Superset Proxy] Error:", err.message);
|
||||
if (res && typeof (res as Response).status === "function") {
|
||||
(res as Response).status(502).json({
|
||||
error: "Superset indisponível",
|
||||
message: "O Superset está iniciando. Tente novamente em alguns segundos.",
|
||||
target,
|
||||
});
|
||||
}
|
||||
},
|
||||
proxyRes: (proxyRes) => {
|
||||
const location = proxyRes.headers["location"];
|
||||
if (location && typeof location === "string" && location.startsWith("/")) {
|
||||
proxyRes.headers["location"] = `/superset${location}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app.use("/superset", supersetProxy);
|
||||
console.log(`[Superset Proxy] Configurado -> /superset/* => ${target}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 Rotas Superset — Guest Token para Embedding
|
||||
|
||||
Esta é a peça-chave para "funcionar em qualquer tela":
|
||||
|
||||
```typescript
|
||||
// server/superset/routes.ts
|
||||
|
||||
import type { Express, Request, Response } from "express";
|
||||
|
||||
const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost";
|
||||
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
|
||||
const SUPERSET_URL = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
|
||||
const SUPERSET_ADMIN_USER = process.env.SUPERSET_ADMIN_USER || "admin";
|
||||
const SUPERSET_ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
|
||||
|
||||
// Cache do token de serviço (expira em 50 min)
|
||||
let serviceToken: string | null = null;
|
||||
let serviceTokenExpiry = 0;
|
||||
|
||||
async function getServiceToken(): Promise<string> {
|
||||
if (serviceToken && Date.now() < serviceTokenExpiry) return serviceToken;
|
||||
|
||||
const resp = await fetch(`${SUPERSET_URL}/api/v1/security/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
username: SUPERSET_ADMIN_USER,
|
||||
password: SUPERSET_ADMIN_PASS,
|
||||
provider: "db",
|
||||
refresh: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!resp.ok) throw new Error("Falha ao autenticar no Superset");
|
||||
const data = await resp.json();
|
||||
serviceToken = data.access_token;
|
||||
serviceTokenExpiry = Date.now() + 50 * 60 * 1000; // 50 min
|
||||
return serviceToken!;
|
||||
}
|
||||
|
||||
export function registerSupersetRoutes(app: Express): void {
|
||||
|
||||
// Guest Token — para embedding em qualquer tela
|
||||
// O frontend chama este endpoint para obter um token temporário
|
||||
// e renderiza o dashboard via @superset-ui/embedded-sdk
|
||||
app.post("/api/superset/guest-token", async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||
|
||||
const { dashboardId } = req.body;
|
||||
if (!dashboardId) return res.status(400).json({ error: "dashboardId obrigatório" });
|
||||
|
||||
const token = await getServiceToken();
|
||||
|
||||
// Solicita guest token ao Superset
|
||||
const guestResp = await fetch(`${SUPERSET_URL}/api/v1/security/guest_token/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user: {
|
||||
username: `arcadia_${req.user?.id}`,
|
||||
first_name: req.user?.name?.split(" ")[0] || "User",
|
||||
last_name: req.user?.name?.split(" ")[1] || "",
|
||||
},
|
||||
resources: [{ type: "dashboard", id: dashboardId }],
|
||||
rls: [], // Row Level Security (futuro: por tenantId)
|
||||
}),
|
||||
});
|
||||
|
||||
if (!guestResp.ok) {
|
||||
const err = await guestResp.text();
|
||||
return res.status(502).json({ error: "Falha ao gerar guest token", detail: err });
|
||||
}
|
||||
|
||||
const { token: guestToken } = await guestResp.json();
|
||||
res.json({ token: guestToken, supersetUrl: "/superset" });
|
||||
} catch (err: any) {
|
||||
res.status(502).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Lista dashboards disponíveis (para o seletor na UI)
|
||||
app.get("/api/superset/dashboards", async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||
|
||||
const token = await getServiceToken();
|
||||
const resp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc)`, {
|
||||
headers: { "Authorization": `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!resp.ok) return res.status(502).json({ error: "Superset indisponível" });
|
||||
const data = await resp.json();
|
||||
res.json(data.result || []);
|
||||
} catch (err: any) {
|
||||
res.status(502).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get("/api/superset/health", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const resp = await fetch(`${SUPERSET_URL}/health`);
|
||||
res.json({ online: resp.ok, url: SUPERSET_URL });
|
||||
} catch {
|
||||
res.json({ online: false, url: SUPERSET_URL });
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4.6 Componente React — SupersetDashboard (reutilizável em QUALQUER tela)
|
||||
|
||||
```typescript
|
||||
// client/src/components/SupersetDashboard.tsx
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { embedDashboard } from "@superset-ui/embedded-sdk";
|
||||
|
||||
interface SupersetDashboardProps {
|
||||
dashboardId: string;
|
||||
height?: string | number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SupersetDashboard({
|
||||
dashboardId,
|
||||
height = "calc(100vh - 300px)",
|
||||
className = "",
|
||||
}: SupersetDashboardProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
let mounted = true;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
embedDashboard({
|
||||
id: dashboardId,
|
||||
supersetDomain: window.location.origin + "/superset",
|
||||
mountPoint: containerRef.current,
|
||||
|
||||
// Busca guest token do gateway Arcádia
|
||||
fetchGuestToken: async () => {
|
||||
const resp = await fetch("/api/superset/guest-token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ dashboardId }),
|
||||
credentials: "include",
|
||||
});
|
||||
if (!resp.ok) throw new Error("Falha ao obter token");
|
||||
const { token } = await resp.json();
|
||||
return token;
|
||||
},
|
||||
|
||||
dashboardUiConfig: {
|
||||
hideTitle: true, // Título próprio no Arcádia
|
||||
hideChartControls: false, // Manter controles de chart
|
||||
filters: {
|
||||
visible: true, // Mostrar filtros
|
||||
expanded: false,
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
if (mounted) setLoading(false);
|
||||
}).catch((err) => {
|
||||
if (mounted) {
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => { mounted = false; };
|
||||
}, [dashboardId]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-48 text-red-500 text-sm">
|
||||
Superset indisponível: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`} style={{ height }}>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/80 z-10">
|
||||
<div className="text-center text-[#1f334d]">
|
||||
<div className="w-8 h-8 border-4 border-[#c89b3c] border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<p className="text-sm">Carregando Arcádia Insights...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={containerRef} className="w-full h-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Uso em QUALQUER página:**
|
||||
```tsx
|
||||
// Exemplo: na página /financeiro
|
||||
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
||||
|
||||
// Dashboard financeiro embeddado diretamente no módulo Financeiro
|
||||
<SupersetDashboard dashboardId="financial-overview" height={500} />
|
||||
|
||||
// Exemplo: na página /contabil
|
||||
<SupersetDashboard dashboardId="dre-mensal" />
|
||||
|
||||
// Exemplo: no Dashboard principal (/)
|
||||
<SupersetDashboard dashboardId="executive-summary" height={400} />
|
||||
```
|
||||
|
||||
### 4.7 BiWorkspace.tsx — Advanced tab (única mudança na UI)
|
||||
|
||||
```tsx
|
||||
// Substituir a tab Advanced (linhas 2918-2945)
|
||||
|
||||
<TabsContent value="advanced" className="mt-0">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[#1f334d]">Arcádia Insights — Apache Superset</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
SQL Lab avançado, 50+ tipos de gráfico, dashboards interativos
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/superset"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[#1f334d] text-white rounded-lg hover:bg-[#2a4466] transition-colors text-sm"
|
||||
>
|
||||
<ArrowRight className="w-4 h-4" /> Abrir em Nova Aba
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Dashboard selecionável */}
|
||||
<SupersetDashboardSelector />
|
||||
</div>
|
||||
</TabsContent>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. TABELAS E VARIÁVEIS
|
||||
|
||||
### 5.1 Nenhuma tabela nova no schema Arcádia
|
||||
|
||||
O Superset usa seu próprio banco (`arcadia_superset`) para metadados internos. As tabelas do Arcádia não mudam.
|
||||
|
||||
### 5.2 Novas variáveis de ambiente
|
||||
|
||||
```bash
|
||||
# .env — adicionar/substituir:
|
||||
|
||||
# Superset (substitui METABASE_*)
|
||||
SUPERSET_HOST=superset # nome do container Docker
|
||||
SUPERSET_PORT=8088
|
||||
SUPERSET_SECRET_KEY= # gerar: openssl rand -hex 32
|
||||
SUPERSET_ADMIN_USER=admin
|
||||
SUPERSET_ADMIN_EMAIL=admin@arcadia.app
|
||||
SUPERSET_ADMIN_PASSWORD= # senha forte em prod
|
||||
|
||||
# URL do banco Arcádia para o Superset acessar (read-only ideal)
|
||||
ARCADIA_DATABASE_URL=postgresql://arcadia_ro:pass@db:5432/arcadia
|
||||
|
||||
# Remover (não são mais necessários):
|
||||
# METABASE_HOST
|
||||
# METABASE_PORT
|
||||
# METASET_ADMIN_EMAIL
|
||||
# METASET_ADMIN_PASSWORD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. ROADMAP — Fases de Implementação
|
||||
|
||||
### Fase 1 — Infraestrutura (2-3 dias)
|
||||
```
|
||||
[ ] Criar docker/superset/superset_config.py
|
||||
[ ] Criar docker/superset/init.sh
|
||||
[ ] Atualizar docker-compose.yml (expandir seção superset existente)
|
||||
[ ] Adicionar volumes: superset_home no docker-compose
|
||||
[ ] Testar: docker compose --profile bi up superset
|
||||
[ ] Verificar: banco arcadia_superset criado, admin funciona, :8088 acessível
|
||||
```
|
||||
|
||||
### Fase 2 — Gateway (1-2 dias)
|
||||
```
|
||||
[ ] Criar server/superset/proxy.ts (substitui server/metabase/proxy.ts)
|
||||
[ ] Criar server/superset/routes.ts (guest token, lista dashboards, health)
|
||||
[ ] Registrar no server/routes.ts: setupSupersetProxy() + registerSupersetRoutes()
|
||||
[ ] Remover/comentar setupMetabaseProxy() e registerMetaSetRoutes()
|
||||
[ ] Testar: GET /api/superset/health → { online: true }
|
||||
[ ] Testar: POST /api/superset/guest-token → token JWT
|
||||
```
|
||||
|
||||
### Fase 3 — Frontend (2-3 dias)
|
||||
```
|
||||
[ ] npm install @superset-ui/embedded-sdk
|
||||
[ ] Criar client/src/components/SupersetDashboard.tsx
|
||||
[ ] Atualizar BiWorkspace.tsx Advanced tab (substituir iframe MetaSet)
|
||||
[ ] Testar: dashboard embeddado na Advanced tab
|
||||
[ ] Criar 3 dashboards base no Superset:
|
||||
- executive-summary (KPIs gerais)
|
||||
- financial-overview (Financeiro)
|
||||
- dre-mensal (DRE)
|
||||
```
|
||||
|
||||
### Fase 4 — Embedding em outras telas (1 semana)
|
||||
```
|
||||
[ ] Embedar SupersetDashboard no /financeiro (tab Análise)
|
||||
[ ] Embedar SupersetDashboard no /contabil (tab DRE/Balanço)
|
||||
[ ] Embedar SupersetDashboard no SOE (tab Dashboard)
|
||||
[ ] Embedar SupersetDashboard no Home / Dashboard principal
|
||||
[ ] RLS por tenantId (Row Level Security nos dashboards)
|
||||
```
|
||||
|
||||
### Fase 5 — Migração de dashboards (conforme necessário)
|
||||
```
|
||||
[ ] Recriar no Superset os dashboards que existiam no MetaSet (se houver)
|
||||
[ ] Configurar alertas automáticos (Superset Alerts & Reports)
|
||||
[ ] Criar dashboards para cada domínio do SOE:
|
||||
- Vendas, Compras, Fiscal, Pessoas, Estoque
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. DIAGRAMA BI ATUALIZADO (completo)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ ARCÁDIA BI STACK v2 │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ FRONTEND REACT — Pode usar em QUALQUER rota │ │
|
||||
│ │ │ │
|
||||
│ │ BiWorkspace.tsx → 8 tabs (7 iguais + Advanced=Superset) │ │
|
||||
│ │ <SupersetDashboard> → componente reutilizável │ │
|
||||
│ │ Guest Token API → /api/superset/guest-token │ │
|
||||
│ └────────────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────┼──────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ┌──────▼──────┐ ┌────────▼───────┐ ┌─────────▼──────────┐ │
|
||||
│ │ API BI │ │ BI Engine │ │ Apache Superset │ │
|
||||
│ │ (Node.js) │ │ Python:8004 │ │ Python:8088 │ │
|
||||
│ │ sem mudança│ │ sem mudança │ │ │ │
|
||||
│ │ │ │ │ │ SQL Lab │ │
|
||||
│ │ CRUD │ │ SQL+Charts │ │ 50+ chart types │ │
|
||||
│ │ Upload/ETL │ │ Micro-BI │ │ Guest Token (embed) │ │
|
||||
│ │ Staging │ │ Análise Pandas │ │ Alerts & Reports │ │
|
||||
│ │ Backups │ │ Cache 5min │ │ REST API v1 │ │
|
||||
│ └─────┬───────┘ └────────┬───────┘ └─────────┬───────────┘ │
|
||||
│ │ │ │ │
|
||||
│ └──────────────────────┼─────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────▼──────────┐ │
|
||||
│ │ PostgreSQL │ │
|
||||
│ │ │ │
|
||||
│ │ arcadia │ ← Dados SOE (read-only) │
|
||||
│ │ arcadia_superset │ ← Metadados Superset │
|
||||
│ └────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||||
│ │ CIENTISTA (Python) — IA/ML: análise, padrões, insights │ │
|
||||
│ │ ASSISTENTE BI (GPT) — chat sobre dados │ │
|
||||
│ │ (ambos sem mudança) │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. RESUMO — O que muda, o que fica
|
||||
|
||||
| Componente | Ação | Esforço |
|
||||
|---|---|---|
|
||||
| `server/metabase/proxy.ts` | Substituir por `server/superset/proxy.ts` | 30 min |
|
||||
| `server/metaset/routes.ts` | Substituir por `server/superset/routes.ts` | 2h |
|
||||
| `server/metaset/client.ts` | Remover (lógica vai para routes.ts) | 5 min |
|
||||
| `docker-compose.yml` Superset | Expandir configuração existente | 1h |
|
||||
| `docker/superset/superset_config.py` | Criar (novo arquivo) | 30 min |
|
||||
| `docker/superset/init.sh` | Criar (novo arquivo) | 1h |
|
||||
| `BiWorkspace.tsx` Advanced tab | Substituir iframe MetaSet | 1h |
|
||||
| `SupersetDashboard.tsx` | Criar componente (novo) | 2h |
|
||||
| `npm install @superset-ui/embedded-sdk` | Instalar dependência | 5 min |
|
||||
| BI Engine Python (:8004) | **Não muda nada** | — |
|
||||
| API BI Node /api/bi/* | **Não muda nada** | — |
|
||||
| Upload/ETL/Staging | **Não muda nada** | — |
|
||||
| 7 tabs do BiWorkspace | **Não muda nada** | — |
|
||||
| Tabelas do banco | **Não muda nada** | — |
|
||||
|
||||
**Total estimado: 1-2 dias de implementação real** (a infra já está 80% pronta).
|
||||
|
||||
---
|
||||
|
||||
## 9. VANTAGEM ESTRATÉGICA
|
||||
|
||||
> O Apache Superset não é apenas "trocar Metabase".
|
||||
>
|
||||
> É a virada de chave que permite que o BI deixe de ser uma página separada
|
||||
> e vire um **componente vivo** que aparece **dentro de cada módulo do Arcádia**:
|
||||
>
|
||||
> - O usuário está no módulo Financeiro → vê o dashboard financeiro ali mesmo
|
||||
> - O usuário está no módulo Contábil → vê o DRE ali mesmo
|
||||
> - O usuário está no SOE → vê os KPIs do negócio ali mesmo
|
||||
>
|
||||
> Sem abrir nova aba. Sem sair do contexto. Sem re-autenticar.
|
||||
> O Superset renderiza no lugar exato onde o usuário está.
|
||||
|
||||
---
|
||||
|
||||
*PLANO_BI_SUPERSET.md — Arcádia Suite v3.0*
|
||||
*Gerado em: 2026-03-16 — Baseado na análise do MAPA_BI_ARCADIA.md (Replit) + código real*
|
||||
|
|
@ -0,0 +1,421 @@
|
|||
# PLANO ESTRATÉGICO DE EVOLUÇÃO
|
||||
## Arcádia Suite → Frappe Framework
|
||||
### Versão 1.0 - Janeiro 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. VISÃO GERAL
|
||||
|
||||
### 1.1 Objetivo
|
||||
Evoluir o Arcádia Suite para um **Business Operating System** completo, inspirado em três referências:
|
||||
|
||||
| Referência | O que Inspira |
|
||||
|------------|---------------|
|
||||
| **Notion** | Blocos modulares, banco de dados relacional, personalização |
|
||||
| **Replit** | IDE no navegador, colaboração em tempo real, deploy instantâneo |
|
||||
| **Discord** | Comunidades, canais contextuais, comunicação em tempo real |
|
||||
|
||||
### 1.2 Estratégia de Migração: Strangler Fig
|
||||
|
||||
A estratégia Strangler Fig permite:
|
||||
- Manter WhatsApp, Manus, CRM funcionando durante toda a migração
|
||||
- Construir o novo sistema em paralelo
|
||||
- Migrar módulo a módulo até o sistema legado "desaparecer"
|
||||
|
||||
```
|
||||
Sistema Atual (Express/React) continua funcionando
|
||||
↓
|
||||
Você vai adicionando "camadas Frappe" por cima
|
||||
↓
|
||||
Cada módulo migrado substitui o antigo
|
||||
↓
|
||||
No final, o "núcleo antigo" sumiu naturalmente
|
||||
```
|
||||
|
||||
### 1.3 Stack Técnico
|
||||
|
||||
| Camada | Atual | Futuro |
|
||||
|--------|-------|--------|
|
||||
| Frontend | React 18 + TypeScript | Frappe Desk + React |
|
||||
| Backend | Express.js | Frappe Framework |
|
||||
| Database | PostgreSQL | PostgreSQL (mesmo) |
|
||||
| Real-time | Socket.IO | Frappe Realtime + Socket.IO |
|
||||
| WhatsApp | Baileys | Frappe App (Baileys) |
|
||||
| IDE | Monaco + Terminal | IDE 3 Modos |
|
||||
|
||||
---
|
||||
|
||||
## 2. ESTRUTURA MULTI-TENANT
|
||||
|
||||
### 2.1 Hierarquia de 3 Níveis
|
||||
|
||||
```
|
||||
NÍVEL 1: MASTER (Arcádia)
|
||||
═════════════════════════
|
||||
• Equipe de desenvolvimento
|
||||
• Acesso total ao sistema
|
||||
• IDE Pro-Code completa
|
||||
• Central de Bibliotecas (publica apps)
|
||||
• Suporte N3 (acessa tenants para debug)
|
||||
• Gerencia parceiros e planos
|
||||
│
|
||||
├───────────────────────┬───────────────────────┐
|
||||
▼ ▼ ▼
|
||||
NÍVEL 2: PARCEIROS
|
||||
══════════════════
|
||||
• Consultorias, integradores, revendas
|
||||
• IDE Low-Code
|
||||
• Gerencia seus clientes
|
||||
• Comissões sobre vendas
|
||||
• Suporte N2 aos clientes
|
||||
• Baixa apps da biblioteca
|
||||
│
|
||||
┌────┴────┐
|
||||
▼ ▼
|
||||
NÍVEL 3: CLIENTES
|
||||
═════════════════
|
||||
• Empresas usuárias finais
|
||||
• Cockpit personalizado
|
||||
• CRM/ERP operacional
|
||||
• WhatsApp (N sessões conforme plano)
|
||||
• BI próprio
|
||||
• Manus com tools básicas
|
||||
```
|
||||
|
||||
### 2.2 Matriz de Permissões por Tipo de Tenant
|
||||
|
||||
| Módulo | Master | Parceiro | Cliente |
|
||||
|--------|--------|----------|---------|
|
||||
| **IDE Pro-Code** | ✅ | ❌ | ❌ |
|
||||
| **IDE Low-Code** | ✅ | ✅ | ❌ |
|
||||
| **IDE No-Code** | ✅ | ✅ | ✅ (se habilitado) |
|
||||
| **Central de Bibliotecas** | ✅ Publicar | ✅ Baixar | ❌ |
|
||||
| **Central de APIs** | ✅ Gerenciar | ⚠️ Seus conectores | ⚠️ Leitura |
|
||||
| **WhatsApp** | ✅ Ilimitado | ✅ N sessões | ✅ N sessões |
|
||||
| **CRM/ERP** | ✅ Global | ✅ Próprio | ✅ Próprio |
|
||||
| **Manus (IA)** | ✅ Todas tools | ✅ Tools permitidas | ✅ Básicas |
|
||||
| **BI/Relatórios** | ✅ Global | ✅ Próprio | ✅ Próprio |
|
||||
| **Suporte N3** | ✅ Acessa tenants | ❌ | ❌ |
|
||||
| **Ver Parceiros** | ✅ | ✅ Seus clientes | ❌ |
|
||||
| **Comissões** | ✅ Gerencia | ✅ Visualiza suas | ❌ |
|
||||
|
||||
### 2.3 Alterações no Schema
|
||||
|
||||
```sql
|
||||
-- Alterações na tabela tenants
|
||||
ALTER TABLE tenants ADD COLUMN tenant_type TEXT DEFAULT 'client';
|
||||
-- master = Arcádia, partner = Parceiros, client = Clientes
|
||||
|
||||
ALTER TABLE tenants ADD COLUMN parent_tenant_id INTEGER REFERENCES tenants(id);
|
||||
-- Referência ao tenant pai (hierarquia)
|
||||
|
||||
ALTER TABLE tenants ADD COLUMN partner_code TEXT;
|
||||
-- Código do parceiro para rastreamento
|
||||
|
||||
ALTER TABLE tenants ADD COLUMN max_users INTEGER DEFAULT 5;
|
||||
ALTER TABLE tenants ADD COLUMN max_storage_mb INTEGER DEFAULT 1000;
|
||||
ALTER TABLE tenants ADD COLUMN features JSONB;
|
||||
ALTER TABLE tenants ADD COLUMN commission_rate NUMERIC(5,2);
|
||||
ALTER TABLE tenants ADD COLUMN trial_ends_at TIMESTAMP;
|
||||
|
||||
-- Nova tabela: Planos
|
||||
CREATE TABLE tenant_plans (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
tenant_type TEXT NOT NULL,
|
||||
max_users INTEGER DEFAULT 5,
|
||||
max_storage_mb INTEGER DEFAULT 1000,
|
||||
features JSONB,
|
||||
monthly_price INTEGER DEFAULT 0,
|
||||
yearly_price INTEGER DEFAULT 0,
|
||||
is_active TEXT DEFAULT 'true',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Nova tabela: Relacionamento Parceiro-Cliente
|
||||
CREATE TABLE partner_clients (
|
||||
id SERIAL PRIMARY KEY,
|
||||
partner_id INTEGER NOT NULL REFERENCES tenants(id),
|
||||
client_id INTEGER NOT NULL REFERENCES tenants(id),
|
||||
commission_rate NUMERIC(5,2),
|
||||
status TEXT DEFAULT 'active',
|
||||
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Nova tabela: Comissões
|
||||
CREATE TABLE partner_commissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
partner_id INTEGER NOT NULL REFERENCES tenants(id),
|
||||
client_id INTEGER NOT NULL REFERENCES tenants(id),
|
||||
reference_month TEXT NOT NULL,
|
||||
client_plan_value INTEGER NOT NULL,
|
||||
commission_rate NUMERIC(5,2) NOT NULL,
|
||||
commission_value INTEGER NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
paid_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. CRONOGRAMA GERAL (6-9 Meses)
|
||||
|
||||
```
|
||||
Mês 1 Mês 2 Mês 3 Mês 4 Mês 5 Mês 6+
|
||||
├──────────┼──────────┼──────────┼──────────┼──────────┼────────►
|
||||
|
||||
FASE 0 ████████████████████
|
||||
FUNDAÇÃO
|
||||
Setup + Tenants + SSO
|
||||
|
||||
FASE 1 ████████████████████████████
|
||||
INFRAESTRUTURA
|
||||
CRM/ERP + Central APIs + Manus
|
||||
|
||||
FASE 2 ████████████████████████████████
|
||||
EXPERIÊNCIA
|
||||
Cockpit + Comunidades + IDE
|
||||
|
||||
FASE 3 ████████████████████►
|
||||
AUTOMAÇÃO
|
||||
WhatsApp + RPA + Decommission
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. FASE 0: FUNDAÇÃO (Semanas 1-8)
|
||||
|
||||
### 4.1 Objetivo
|
||||
Preparar a base técnica sem quebrar nada do sistema atual.
|
||||
|
||||
### 4.2 Entregas
|
||||
|
||||
| # | Entrega | Descrição | Semana |
|
||||
|---|---------|-----------|--------|
|
||||
| 0.1 | **Setup Frappe Bench** | Instalar Frappe em servidor paralelo | 1-2 |
|
||||
| 0.2 | **Hierarquia de Tenants** | Novos campos e tabelas no PostgreSQL | 2-3 |
|
||||
| 0.3 | **SSO Bridge** | Login unificado (usuário loga uma vez) | 3-4 |
|
||||
| 0.4 | **CDC Pipeline** | Sincronização de dados PostgreSQL ↔ Frappe | 4-6 |
|
||||
| 0.5 | **Vault de Secrets** | Gerenciamento seguro de API keys | 5-6 |
|
||||
| 0.6 | **Feature Flags** | Sistema de features por plano/tenant | 6-7 |
|
||||
| 0.7 | **Planos e Preços** | Tabela de planos (free, starter, pro, enterprise) | 7-8 |
|
||||
|
||||
### 4.3 Resultado
|
||||
- ✅ Frappe rodando em paralelo
|
||||
- ✅ Hierarquia master/partner/client funcionando
|
||||
- ✅ Login único nos dois sistemas
|
||||
- ✅ Dados sincronizados em tempo real
|
||||
- ✅ Planos e features configuráveis
|
||||
|
||||
---
|
||||
|
||||
## 5. FASE 1: INFRAESTRUTURA (Semanas 6-16)
|
||||
|
||||
### 5.1 Objetivo
|
||||
Migrar dados mestres e criar a Central de APIs.
|
||||
|
||||
### 5.2 Entregas
|
||||
|
||||
| # | Entrega | Descrição | Semana |
|
||||
|---|---------|-----------|--------|
|
||||
| 1.1 | **DocTypes CRM** | Clientes, Leads, Oportunidades no Frappe | 6-8 |
|
||||
| 1.2 | **DocTypes ERP** | Produtos, Pedidos, Faturas no Frappe | 8-10 |
|
||||
| 1.3 | **Central de APIs (MVP)** | Dashboard visual de integrações | 9-12 |
|
||||
| 1.4 | **Conectores Básicos** | Interface para SEFAZ, Bancos (dados demo) | 12-14 |
|
||||
| 1.5 | **Manus Frappe** | Agente IA via background jobs | 13-15 |
|
||||
| 1.6 | **Knowledge Graph** | Migração do grafo para DocTypes | 14-16 |
|
||||
|
||||
### 5.3 Central de APIs - Detalhamento
|
||||
|
||||
**IMPORTANTE:** A Central de APIs é uma interface visual de gerenciamento. Os dados de integrações (SEFAZ, Bancos, Mercado Livre) são ILUSTRATIVOS/DEMO. Não fazemos integração real com APIs externas nesta fase.
|
||||
|
||||
O que construímos:
|
||||
- ✅ Interface visual (React)
|
||||
- ✅ CRUD de conectores (cadastrar, editar, remover)
|
||||
- ✅ Status visual (online, warning, error)
|
||||
- ✅ Logs fictícios para demonstração
|
||||
- ✅ Configurações por conector
|
||||
- ✅ Permissões por tenant type
|
||||
|
||||
O que NÃO fazemos:
|
||||
- ❌ Conectar à SEFAZ real
|
||||
- ❌ Conectar a bancos reais
|
||||
- ❌ Chamadas API externas
|
||||
|
||||
### 5.4 Resultado
|
||||
- ✅ CRM/ERP acessível via Frappe Desk
|
||||
- ✅ Central de APIs funcionando (dados demo)
|
||||
- ✅ Manus consultando dados do Frappe
|
||||
- ✅ Knowledge Graph migrado
|
||||
|
||||
---
|
||||
|
||||
## 6. FASE 2: EXPERIÊNCIA (Semanas 12-24)
|
||||
|
||||
### 6.1 Objetivo
|
||||
Construir a nova interface (Cockpit, Comunidades, IDE).
|
||||
|
||||
### 6.2 Entregas
|
||||
|
||||
| # | Entrega | Descrição | Semana |
|
||||
|---|---------|-----------|--------|
|
||||
| 2.1 | **Cockpit PARA** | Navegação Projetos/Áreas/Recursos/Arquivo | 12-15 |
|
||||
| 2.2 | **Dashboard Tríade** | Importante/Urgente/Circunstancial | 14-16 |
|
||||
| 2.3 | **Widgets Sistema** | Tarefas, Calendário, Gráficos | 15-17 |
|
||||
| 2.4 | **Comunidades MVP** | Canais por projeto (Socket.IO via Frappe) | 16-19 |
|
||||
| 2.5 | **IDE No-Code** | DocType Builder visual | 18-20 |
|
||||
| 2.6 | **IDE Low-Code** | Templates de scripts | 20-22 |
|
||||
| 2.7 | **IDE Pro-Code** | Monaco + Terminal + Live Preview | 21-23 |
|
||||
| 2.8 | **Central de Bibliotecas** | Repositório de apps Frappe | 22-24 |
|
||||
|
||||
### 6.3 Cockpit PARA + Tríade
|
||||
|
||||
O Cockpit é a interface principal do usuário, baseado em duas metodologias:
|
||||
|
||||
**Método PARA (Tiago Forte):**
|
||||
- **P**rojetos: Todos os projetos ativos com metas e prazos
|
||||
- **Á**reas: Áreas de responsabilidade contínua (Vendas, Financeiro, RH)
|
||||
- **R**ecursos: Base de conhecimento, templates, manuais
|
||||
- **A**rquivo: Tudo concluído ou inativo, para consulta futura
|
||||
|
||||
**Tríade do Tempo (Christian Barbosa):**
|
||||
- 🟢 **Importante** (70% do tempo): Atividades que geram valor
|
||||
- 🟡 **Urgente** (20% do tempo): Atividades com prazo apertado
|
||||
- 🔴 **Circunstancial** (10% do tempo): Atividades que não agregam
|
||||
|
||||
### 6.4 IDE 3 Modos
|
||||
|
||||
| Modo | Quem Usa | O que Faz |
|
||||
|------|----------|-----------|
|
||||
| **No-Code** | Clientes | Criar formulários arrastando, workflows visuais, relatórios com filtros |
|
||||
| **Low-Code** | Parceiros | Server Scripts com templates, validações, webhooks, fórmulas |
|
||||
| **Pro-Code** | Arcádia | Monaco Editor completo, Terminal, Git, Deploy de apps |
|
||||
|
||||
### 6.5 Resultado
|
||||
- ✅ Cockpit PARA + Tríade funcionando
|
||||
- ✅ Comunidades com canais por projeto
|
||||
- ✅ IDE com 3 modos operando
|
||||
- ✅ Central de Bibliotecas publicando apps
|
||||
|
||||
---
|
||||
|
||||
## 7. FASE 3: AUTOMAÇÃO E DECOMMISSION (Semana 20+)
|
||||
|
||||
### 7.1 Objetivo
|
||||
Migrar serviços restantes e desligar o legado.
|
||||
|
||||
### 7.2 Entregas
|
||||
|
||||
| # | Entrega | Descrição | Semana |
|
||||
|---|---------|-----------|--------|
|
||||
| 3.1 | **WhatsApp Frappe App** | Reconstruir Baileys como app nativo | 20-24 |
|
||||
| 3.2 | **Motor de Workflows** | Automações visuais (RPA) | 22-26 |
|
||||
| 3.3 | **Scientist Frappe** | Migrar para Frappe Workers | 24-28 |
|
||||
| 3.4 | **Validação de Paridade** | Testes A/B, métricas | 26-30 |
|
||||
| 3.5 | **Decommission Express** | Desligar endpoints legados | 30+ |
|
||||
|
||||
### 7.3 Resultado
|
||||
- ✅ Sistema 100% unificado no Frappe
|
||||
- ✅ Express/React desligado
|
||||
- ✅ Uma única plataforma para manter
|
||||
|
||||
---
|
||||
|
||||
## 8. MAPEAMENTO DE MÓDULOS
|
||||
|
||||
| Módulo Atual | O que Acontece | Fase |
|
||||
|--------------|----------------|------|
|
||||
| **users, tenants** | Expande com hierarquia | 0 |
|
||||
| **profiles, roles, permissions** | Migra para Frappe RBAC | 0 |
|
||||
| **whatsapp_contacts, messages, tickets** | Mantém → Migra na Fase 3 | 3 |
|
||||
| **pc_crm_leads, stages, opportunities** | Migra para Frappe DocTypes | 1 |
|
||||
| **pc_clients, projects, tasks** | Migra para Frappe DocTypes | 1 |
|
||||
| **graph_nodes, graph_edges** | Migra para Frappe Knowledge Graph | 1 |
|
||||
| **internal_chat_*** | Evolui para Comunidades | 2 |
|
||||
| **manus_*** | Integra via background jobs | 1 |
|
||||
| **bi_*** | Mantém + novos widgets Cockpit | 2 |
|
||||
| **ide_*** | Evolui para 3 modos | 2 |
|
||||
|
||||
---
|
||||
|
||||
## 9. RISCOS E MITIGAÇÕES
|
||||
|
||||
| Risco | Probabilidade | Impacto | Mitigação |
|
||||
|-------|---------------|---------|-----------|
|
||||
| **Drift de dados** | Média | Alto | CDC com validação contínua |
|
||||
| **Performance chat** | Média | Médio | Load test antes de migrar |
|
||||
| **Tokens WhatsApp** | Baixa | Alto | Vault de secrets |
|
||||
| **Curva aprendizado Frappe** | Alta | Médio | Treinamento na Fase 0 |
|
||||
| **Regressões funcionais** | Média | Alto | Testes A/B, telemetria |
|
||||
| **Resistência usuários** | Média | Médio | Piloto gradual: Master → Partners → Clients |
|
||||
|
||||
---
|
||||
|
||||
## 10. QUICK WINS (Entregas Rápidas)
|
||||
|
||||
| Item | Tempo | Valor |
|
||||
|------|-------|-------|
|
||||
| **Hierarquia de Tenants** | 2 semanas | Estrutura para parceiros |
|
||||
| **SSO unificado** | 2 semanas | Login único |
|
||||
| **Central de APIs (UI)** | 3 semanas | Visibilidade integrações |
|
||||
| **Dashboard Tríade** | 2 semanas | Consciência sobre tempo |
|
||||
| **Planos e Features** | 2 semanas | Monetização estruturada |
|
||||
|
||||
---
|
||||
|
||||
## 11. OS 5 PILARES DO SISTEMA
|
||||
|
||||
### Pilar 1: Knowledge Graph
|
||||
- Todos os dados do negócio conectados e pesquisáveis
|
||||
- Navegação visual entre entidades relacionadas
|
||||
- Base para IA contextual
|
||||
|
||||
### Pilar 2: Central Intelligence (Scientist)
|
||||
- IA que aprende com interações do sistema
|
||||
- Gera e executa código automaticamente
|
||||
- Detecta padrões e sugere otimizações
|
||||
|
||||
### Pilar 3: Autonomous Agent (Manus)
|
||||
- Executa tarefas multi-step de forma autônoma
|
||||
- Acessa ferramentas e APIs
|
||||
- Deep research com planejamento
|
||||
|
||||
### Pilar 4: Unified Communication
|
||||
- WhatsApp integrado com CRM
|
||||
- Chat interno com canais por projeto
|
||||
- Email (futuro)
|
||||
- Todos os canais em um lugar
|
||||
|
||||
### Pilar 5: Complete IDE
|
||||
- 3 modos de desenvolvimento (No/Low/Pro Code)
|
||||
- Central de Bibliotecas
|
||||
- Deploy integrado
|
||||
|
||||
---
|
||||
|
||||
## 12. DOCUMENTOS DE REFERÊNCIA
|
||||
|
||||
Os documentos originais que basearam este plano estão em:
|
||||
- `attached_assets/cocpti_docs/cocpti/` - Cockpit e DNA Notion
|
||||
- `attached_assets/cocpti_docs/Ide Arcadia/` - Proposta IDE
|
||||
- `attached_assets/cocpti_docs/Rota de desenvolviento/` - Roadmap original
|
||||
- `attached_assets/cocpti_docs/Central de API/` - Central de APIs
|
||||
|
||||
---
|
||||
|
||||
## 13. PRÓXIMOS PASSOS
|
||||
|
||||
1. [ ] Implementar hierarquia de tenants no schema
|
||||
2. [ ] Criar tabelas de planos e comissões
|
||||
3. [ ] Documentar arquitetura CDC
|
||||
4. [ ] Provisionar servidor Frappe
|
||||
5. [ ] Implementar Central de APIs (UI com dados demo)
|
||||
6. [ ] Construir Cockpit PARA + Tríade
|
||||
|
||||
---
|
||||
|
||||
*Documento criado em Janeiro 2026*
|
||||
*Última atualização: Janeiro 2026*
|
||||
|
|
@ -0,0 +1,733 @@
|
|||
# PLANO SOE CENTRAL — Arcádia Suite
|
||||
## Orquestração Inteligente: Plus + ERPNext invisíveis, Central visível
|
||||
### Versão 1.0 — Março 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. ONDE ESTAMOS (Orientação Geral)
|
||||
|
||||
### O Projeto nasceu no Replit como…
|
||||
- Uma plataforma de **escritório empresarial com IA central** (Manus Agent)
|
||||
- Com proxy para o **Arcádia Plus** (Laravel/PHP, porta 8080) — ERP fiscal completo
|
||||
- Com integração ao **ERPNext/Frappe** via API
|
||||
- Stack principal: **React 18 + Express.js + PostgreSQL + FastAPI (Python)**
|
||||
|
||||
### O que JÁ existe e funciona:
|
||||
| Componente | Status | Onde vive |
|
||||
|---|---|---|
|
||||
| Manus Agent (IA, 30+ tools) | ✅ Funcionando | `server/manus/` |
|
||||
| Arcádia Plus (Laravel ERP) | ✅ Funcionando | `plus/` → porta 8080 |
|
||||
| SSO Plus ↔ Suite | ✅ Funcionando | `server/plus/` |
|
||||
| Motor Fiscal (nfelib) | ✅ Funcionando | `server/python/` → porta 8002 |
|
||||
| Motor Contábil | ✅ Funcionando | `server/python/` → porta 8003 |
|
||||
| Motor BI | ✅ Funcionando | `server/python/` → porta 8004 |
|
||||
| Motor Automação | ✅ Funcionando | `server/python/` → porta 8005 |
|
||||
| Motor Comunicação (Comm) | ✅ Funcionando | porta 8006 |
|
||||
| Dev Center XOS (6 agentes) | ✅ Funcionando | `server/blackboard/` |
|
||||
| LiteLLM Gateway | ✅ Configurado | porta 4000 |
|
||||
| CRM, WhatsApp, Chat | ✅ Funcionando | `server/crm/`, `server/whatsapp/` |
|
||||
| ERPNext integração | ⚠️ Parcial | `server/erp/routes.ts` |
|
||||
|
||||
### O que FALTA para o plano atual:
|
||||
| Componente | Status |
|
||||
|---|---|
|
||||
| **SOE Central (Regras Unificadas)** | ❌ Não existe — este é o plano |
|
||||
| Fiscal rules centralizadas (acima do Plus) | ❌ Dispersas no Plus |
|
||||
| ERPNext rules centralizadas | ❌ Não existe camada de abstração |
|
||||
| Contábil Avançado (lançamentos automáticos) | ❌ Motor existe, regras não |
|
||||
| Financeiro Avançado (previsão, DRE automático) | ❌ Motor existe, regras não |
|
||||
| Testes automatizados / CI-CD | ❌ |
|
||||
| Monitoramento (APM, Sentry) | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 2. O PROBLEMA QUE VAMOS RESOLVER
|
||||
|
||||
### Situação Atual (problemática)
|
||||
```
|
||||
Usuário → Arcádia Suite
|
||||
├── /plus → acessa Laravel diretamente (usuário VÊ o Plus)
|
||||
├── /erp → acessa ERPNext diretamente (usuário VÊ o ERPNext)
|
||||
└── Regras fiscais ficam dentro do Plus
|
||||
Regras contábeis ficam no Motor 8003
|
||||
Regras ERP ficam no ERPNext
|
||||
→ Fragmentado, sem governança central
|
||||
```
|
||||
|
||||
### Situação Desejada (SOE Central)
|
||||
```
|
||||
Usuário → Arcádia Suite (SOE)
|
||||
│
|
||||
▼ [Central de Regras SOE]
|
||||
├── Fiscal: regras unificadas (CFOP, NCM, tributação, NF-e, CT-e...)
|
||||
├── ERP: regras de negócio (pedidos, estoque, compras...)
|
||||
├── Contábil: regras de lançamentos, plano de contas, DRE...
|
||||
└── Financeiro: regras de fluxo, provisões, conciliação...
|
||||
│
|
||||
▼ [Execução invisível]
|
||||
├── Plus → emite documentos, registra no banco
|
||||
└── ERPNext → executa operações, registra no banco
|
||||
|
||||
O usuário NUNCA VÊ o Plus nem o ERPNext.
|
||||
Só vê a Central SOE do Arcádia.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. ARQUITETURA SOE CENTRAL
|
||||
|
||||
### 3.1 Diagrama Completo
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ ARCÁDIA SUITE (Frontend React) │
|
||||
│ /financeiro /contabil /fisco /erp /people /production /bi │
|
||||
│ Usuário nunca acessa /plus ou ERPNext diretamente │
|
||||
└───────────────────────────────┬─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ SOE — SISTEMA OPERACIONAL EMPRESARIAL (Nova Camada) │
|
||||
│ Express.js / porta 5000 / server/soe/ │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ CENTRAL FISCAL │ │ CENTRAL ERP │ │
|
||||
│ │ server/soe/fiscal/ │ │ server/soe/erp/ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • Regras NCM/CFOP │ │ • Regras de pedidos │ │
|
||||
│ │ • Políticas tribut. │ │ • Regras de estoque │ │
|
||||
│ │ • Emissão NF-e/NFC-e │ │ • Regras de compras │ │
|
||||
│ │ • Cancelamento │ │ • Regras de pessoas │ │
|
||||
│ │ • SPED/SINTEGRA │ │ • Integrações ext. │ │
|
||||
│ └──────────┬───────────┘ └──────────┬─────────────┘ │
|
||||
│ │ │ │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ CENTRAL CONTÁBIL │ │ CENTRAL FINANCEIRO │ │
|
||||
│ │ server/soe/contabil/│ │ server/soe/financ/ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • Plano de contas │ │ • Fluxo de caixa │ │
|
||||
│ │ • Regras de lanç. │ │ • Contas a P/R │ │
|
||||
│ │ • DRE automático │ │ • Conciliação │ │
|
||||
│ │ • Balancete │ │ • Previsão (IA) │ │
|
||||
│ │ • Fechamento │ │ • Impostos │ │
|
||||
│ └──────────┬───────────┘ └──────────┬─────────────┘ │
|
||||
│ │ │ │
|
||||
│ └────────────┬─────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ SOE RULE ENGINE (Núcleo) │ │
|
||||
│ │ server/soe/rule-engine.ts │ │
|
||||
│ │ │ │
|
||||
│ │ • Resolução de conflitos │ │
|
||||
│ │ • Aplicação de políticas XOS │ │
|
||||
│ │ • Audit trail de todas regras │ │
|
||||
│ │ • Manus consulta este engine │ │
|
||||
│ └─────────────────┬───────────────┘ │
|
||||
└────────────────────────────┼────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐
|
||||
│ ARCÁDIA PLUS │ │ ERPNEXT │ │ MOTORES PYTHON │
|
||||
│ Laravel :8080 │ │ Frappe : │ │ 8002 Fiscal │
|
||||
│ (invisível) │ │ (invisível) │ │ 8003 Contábil │
|
||||
│ │ │ │ │ 8004 BI │
|
||||
│ Emite docs │ │ Executa ERP │ │ 8005 Automação │
|
||||
│ fiscais │ │ operacional │ │ │
|
||||
└─────────────────┘ └──────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. CENTRAL FISCAL — Detalhamento
|
||||
|
||||
### 4.1 O que ela resolve
|
||||
O Plus tem **376 controllers** e **1.546 rotas** — tudo fiscal/operacional. A Central Fiscal **não substitui** o Plus, ela **orquestra** o Plus com regras definidas no SOE.
|
||||
|
||||
### 4.2 Regras que vão para a Central (tiradas do Plus)
|
||||
|
||||
```typescript
|
||||
// server/soe/fiscal/rules.ts — Exemplo de estrutura
|
||||
|
||||
interface FiscalRule {
|
||||
id: string
|
||||
nome: string
|
||||
trigger: 'pre_emissao' | 'pos_emissao' | 'cancelamento' | 'validacao'
|
||||
condicao: FiscalCondition
|
||||
acao: FiscalAction
|
||||
prioridade: number
|
||||
ativo: boolean
|
||||
}
|
||||
|
||||
// Exemplos de regras:
|
||||
const REGRAS_FISCAIS: FiscalRule[] = [
|
||||
{
|
||||
id: 'NFE_CFOP_INTERESTADUAL',
|
||||
nome: 'Ajusta CFOP para operação interestadual',
|
||||
trigger: 'pre_emissao',
|
||||
condicao: { uf_emitente: '!=', uf_destinatario: true },
|
||||
acao: { ajustar_cfop: 'interestadual' },
|
||||
prioridade: 1,
|
||||
ativo: true
|
||||
},
|
||||
{
|
||||
id: 'NFE_SIMPLES_NACIONAL',
|
||||
nome: 'Aplica tributação Simples Nacional',
|
||||
trigger: 'pre_emissao',
|
||||
condicao: { regime_tributario: 'simples_nacional' },
|
||||
acao: { aplicar_cst: 'tabela_simples', remover_pis_cofins: true },
|
||||
prioridade: 2,
|
||||
ativo: true
|
||||
},
|
||||
{
|
||||
id: 'NFE_VALIDACAO_NCM',
|
||||
nome: 'Valida NCM obrigatório para NF-e',
|
||||
trigger: 'validacao',
|
||||
condicao: { tipo_documento: 'nfe' },
|
||||
acao: { validar_campo: 'ncm', obrigatorio: true },
|
||||
prioridade: 1,
|
||||
ativo: true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 4.3 Fluxo de Emissão via SOE Central
|
||||
|
||||
```
|
||||
Usuário clica "Emitir NF-e" no Arcádia
|
||||
│
|
||||
▼
|
||||
POST /api/soe/fiscal/emitir-nfe
|
||||
│
|
||||
▼
|
||||
SOE Rule Engine: aplica REGRAS_FISCAIS (pre_emissao)
|
||||
• Verifica regime tributário
|
||||
• Aplica CFOP correto
|
||||
• Calcula impostos (ICMS, PIS, COFINS, IPI)
|
||||
• Valida NCM obrigatório
|
||||
• Adiciona dados da empresa emitente
|
||||
│
|
||||
▼
|
||||
SOE envia para Plus via API interna (invisível)
|
||||
POST http://plus:8080/api/nfe/emitir
|
||||
+ token SSO automático
|
||||
│
|
||||
▼
|
||||
Plus emite via Cloud-DFE → SEFAZ
|
||||
│
|
||||
▼
|
||||
SOE recebe retorno
|
||||
• Registra no banco SOE (tabela soe_fiscal_eventos)
|
||||
• Dispara lançamento contábil automático
|
||||
• Notifica via WebSocket o frontend
|
||||
│
|
||||
▼
|
||||
Usuário vê resultado no Arcádia (nunca soube do Plus)
|
||||
```
|
||||
|
||||
### 4.4 Tabelas SOE para Fiscal
|
||||
|
||||
```sql
|
||||
-- Tabelas a criar no schema Arcádia (PostgreSQL)
|
||||
|
||||
CREATE TABLE soe_fiscal_regras (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
nome VARCHAR(200) NOT NULL,
|
||||
trigger VARCHAR(50) NOT NULL,
|
||||
condicao JSONB NOT NULL,
|
||||
acao JSONB NOT NULL,
|
||||
prioridade INTEGER DEFAULT 10,
|
||||
ativo BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE soe_fiscal_eventos (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tipo VARCHAR(50) NOT NULL, -- 'emissao_nfe', 'cancelamento', etc.
|
||||
empresa_id INTEGER NOT NULL,
|
||||
documento_chave VARCHAR(100),
|
||||
regras_aplicadas JSONB, -- quais regras foram aplicadas
|
||||
payload_enviado JSONB, -- o que foi enviado ao Plus
|
||||
payload_retorno JSONB, -- o que o Plus/SEFAZ respondeu
|
||||
status VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE soe_fiscal_configuracoes (
|
||||
empresa_id INTEGER PRIMARY KEY,
|
||||
regime_tributario VARCHAR(50), -- simples, lucro_presumido, lucro_real
|
||||
uf VARCHAR(2),
|
||||
cnpj VARCHAR(18),
|
||||
certificado_tipo VARCHAR(20),
|
||||
aliquotas JSONB, -- alíquotas padrão por operação
|
||||
cfops_padrao JSONB, -- CFOPs padrão por tipo de operação
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. CENTRAL ERP (ERPNext/Frappe) — Detalhamento
|
||||
|
||||
### 5.1 O que ela resolve
|
||||
O ERPNext tem **APIs complexas**. A Central ERP abstrai tudo isso em operações simples que o Arcádia entende.
|
||||
|
||||
### 5.2 Adapter Layer ERPNext
|
||||
|
||||
```typescript
|
||||
// server/soe/erp/frappe-adapter.ts
|
||||
|
||||
interface ERPAdapter {
|
||||
// Produtos
|
||||
criarProduto(data: ProdutoSOE): Promise<ERPProduto>
|
||||
buscarProduto(codigo: string): Promise<ProdutoSOE>
|
||||
atualizarEstoque(movimentacao: MovimentacaoEstoque): Promise<void>
|
||||
|
||||
// Pedidos
|
||||
criarPedidoVenda(pedido: PedidoSOE): Promise<ERPPedido>
|
||||
aprovarPedido(id: string): Promise<void>
|
||||
faturarPedido(id: string): Promise<ERPFatura>
|
||||
|
||||
// Financeiro
|
||||
criarLancamento(lancamento: LancamentoSOE): Promise<ERPLancamento>
|
||||
buscarPlanoContas(): Promise<ContaContabil[]>
|
||||
|
||||
// Compras
|
||||
criarPedidoCompra(data: PedidoCompraSOE): Promise<ERPPedidoCompra>
|
||||
receberMercadoria(data: RecebimentoSOE): Promise<void>
|
||||
}
|
||||
|
||||
// As regras ficam no SOE, o adapter só traduz
|
||||
class FrappeAdapter implements ERPAdapter {
|
||||
private baseUrl: string
|
||||
private apiKey: string
|
||||
|
||||
async criarPedidoVenda(pedido: PedidoSOE): Promise<ERPPedido> {
|
||||
// Traduz do modelo SOE para o modelo Frappe
|
||||
const frappePedido = this.traduzirParaFrappe(pedido)
|
||||
|
||||
// Envia para ERPNext (invisível para o usuário)
|
||||
const response = await fetch(`${this.baseUrl}/api/resource/Sales Order`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `token ${this.apiKey}` },
|
||||
body: JSON.stringify(frappePedido)
|
||||
})
|
||||
|
||||
// Traduz retorno de volta para modelo SOE
|
||||
return this.traduzirDeVoltaSOE(await response.json())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Regras de Negócio ERP no SOE
|
||||
|
||||
```typescript
|
||||
// server/soe/erp/rules.ts
|
||||
|
||||
const REGRAS_ERP = [
|
||||
{
|
||||
id: 'PEDIDO_APROVACAO_AUTOMATICA',
|
||||
nome: 'Aprovação automática até R$ 5.000',
|
||||
trigger: 'pre_aprovacao_pedido',
|
||||
condicao: { valor_total: { menor_que: 5000 } },
|
||||
acao: { aprovar_automaticamente: true }
|
||||
},
|
||||
{
|
||||
id: 'ESTOQUE_RESERVA_VENDA',
|
||||
nome: 'Reservar estoque ao confirmar venda',
|
||||
trigger: 'pos_confirmacao_venda',
|
||||
condicao: { status: 'confirmado' },
|
||||
acao: { reservar_estoque: true, gerar_separacao: true }
|
||||
},
|
||||
{
|
||||
id: 'NF_AUTOMATICA_FATURAMENTO',
|
||||
nome: 'Emite NF-e automaticamente ao faturar',
|
||||
trigger: 'pos_faturamento',
|
||||
condicao: { tipo_cliente: ['pj', 'pf_contribuinte'] },
|
||||
acao: { emitir_nfe: true, via_soe_fiscal: true }
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. CENTRAL CONTÁBIL AVANÇADA
|
||||
|
||||
### 6.1 Motor Atual (porta 8003) + Regras SOE
|
||||
|
||||
O Motor Contábil Python já faz DRE e Balancete. O que falta é **alimentação automática**.
|
||||
|
||||
### 6.2 Lançamentos Automáticos (grande salto)
|
||||
|
||||
```typescript
|
||||
// server/soe/contabil/auto-lancamentos.ts
|
||||
|
||||
interface RegrasLancamentoAuto {
|
||||
gatilho: string
|
||||
debito: string // Conta contábil
|
||||
credito: string // Conta contábil
|
||||
historico: string
|
||||
formula: string // Como calcular o valor
|
||||
}
|
||||
|
||||
const LANCAMENTOS_AUTOMATICOS: RegrasLancamentoAuto[] = [
|
||||
// Quando emite NF-e de venda
|
||||
{
|
||||
gatilho: 'emissao_nfe_venda',
|
||||
debito: '1.1.3.01', // Clientes
|
||||
credito: '3.1.1.01', // Receita Bruta de Vendas
|
||||
historico: 'Venda conforme NF-e {numero}',
|
||||
formula: 'valor_total_nota'
|
||||
},
|
||||
{
|
||||
gatilho: 'emissao_nfe_venda',
|
||||
debito: '3.1.2.01', // Dedução de Receita (ICMS)
|
||||
credito: '2.1.2.01', // ICMS a Recolher
|
||||
historico: 'ICMS s/ Venda NF-e {numero}',
|
||||
formula: 'valor_icms'
|
||||
},
|
||||
// Quando paga conta
|
||||
{
|
||||
gatilho: 'pagamento_conta',
|
||||
debito: '2.1.1.{categoria}', // Conta a Pagar específica
|
||||
credito: '1.1.1.01', // Banco/Caixa
|
||||
historico: 'Pagamento: {descricao}',
|
||||
formula: 'valor_pago'
|
||||
},
|
||||
// Quando recebe pagamento
|
||||
{
|
||||
gatilho: 'recebimento',
|
||||
debito: '1.1.1.01', // Banco/Caixa
|
||||
credito: '1.1.3.01', // Clientes
|
||||
historico: 'Recebimento: {descricao}',
|
||||
formula: 'valor_recebido'
|
||||
},
|
||||
// Compra de mercadoria
|
||||
{
|
||||
gatilho: 'entrada_nfe_compra',
|
||||
debito: '1.1.4.01', // Estoque
|
||||
credito: '2.1.1.01', // Fornecedores
|
||||
historico: 'Compra conforme NF-e {numero} / {fornecedor}',
|
||||
formula: 'valor_mercadorias'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 6.3 DRE Automático via SOE
|
||||
|
||||
```
|
||||
Usuário pede DRE do mês
|
||||
│
|
||||
▼
|
||||
SOE Contábil consulta:
|
||||
1. Todos lançamentos do período (tabela soe_lancamentos)
|
||||
2. Agrupa por conta contábil
|
||||
3. Aplica estrutura DRE configurada
|
||||
4. Envia para Motor Python 8003 para cálculos
|
||||
5. Retorna DRE formatado
|
||||
│
|
||||
▼
|
||||
Usuário vê DRE no /contabil do Arcádia
|
||||
(Nunca soube que teve lançamentos automáticos de NF-e, pagamentos, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. CENTRAL FINANCEIRO AVANÇADO
|
||||
|
||||
### 7.1 Regras Financeiras no SOE
|
||||
|
||||
```typescript
|
||||
// server/soe/financeiro/rules.ts
|
||||
|
||||
const REGRAS_FINANCEIRO = [
|
||||
{
|
||||
id: 'BOLETO_AUTO_VENCIMENTO',
|
||||
nome: 'Gerar boleto automático para vendas a prazo',
|
||||
trigger: 'pos_confirmacao_venda_prazo',
|
||||
acao: { gerar_boleto: true, via: 'asaas', vencimento: '+{prazo}_dias' }
|
||||
},
|
||||
{
|
||||
id: 'ALERTA_VENCIMENTO_3DIAS',
|
||||
nome: 'Alerta de vencimento 3 dias antes',
|
||||
trigger: 'cron_diario',
|
||||
condicao: { vencimento_em: 3 },
|
||||
acao: { notificar: ['whatsapp', 'email'], via_manus: true }
|
||||
},
|
||||
{
|
||||
id: 'PROVISAO_IMPOSTOS',
|
||||
nome: 'Provisão mensal de impostos',
|
||||
trigger: 'cron_mensal',
|
||||
acao: { calcular_provisao: ['icms', 'pis', 'cofins', 'csll', 'irpj'], registrar_lancamento: true }
|
||||
},
|
||||
{
|
||||
id: 'CONCILIACAO_AUTO',
|
||||
nome: 'Conciliação bancária automática via OFX',
|
||||
trigger: 'importacao_ofx',
|
||||
acao: { tentar_conciliar: true, confianca_minima: 0.85, pendentes_para_revisao: true }
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. MANUS AGENT + SOE CENTRAL
|
||||
|
||||
### 8.1 O Manus consulta a Central
|
||||
|
||||
```
|
||||
Pergunta: "Qual foi o resultado do mês passado?"
|
||||
│
|
||||
▼
|
||||
Manus → SOE Central
|
||||
• GET /api/soe/contabil/dre?periodo=2026-02
|
||||
• GET /api/soe/financeiro/fluxo?periodo=2026-02
|
||||
• GET /api/soe/fiscal/resumo?periodo=2026-02
|
||||
│
|
||||
▼
|
||||
SOE agrega dados dos motores (Python) + Plus + ERPNext
|
||||
│
|
||||
▼
|
||||
Manus recebe contexto completo e responde:
|
||||
"Faturamento: R$ 120.000
|
||||
Deduções (impostos): R$ 18.000
|
||||
Receita Líquida: R$ 102.000
|
||||
Custo das mercadorias: R$ 65.000
|
||||
Lucro Bruto: R$ 37.000
|
||||
..."
|
||||
```
|
||||
|
||||
### 8.2 Tools do Manus para o SOE
|
||||
|
||||
```typescript
|
||||
// Novas tools do Manus que consultam o SOE Central
|
||||
|
||||
{
|
||||
name: 'consultar_situacao_fiscal',
|
||||
description: 'Consulta situação fiscal da empresa (NFs pendentes, impostos, SPED)',
|
||||
endpoint: 'GET /api/soe/fiscal/situacao'
|
||||
},
|
||||
{
|
||||
name: 'emitir_documento_fiscal',
|
||||
description: 'Emite NF-e, NFC-e, CT-e ou MDF-e',
|
||||
endpoint: 'POST /api/soe/fiscal/emitir'
|
||||
},
|
||||
{
|
||||
name: 'consultar_dre',
|
||||
description: 'Gera DRE de qualquer período',
|
||||
endpoint: 'GET /api/soe/contabil/dre'
|
||||
},
|
||||
{
|
||||
name: 'consultar_fluxo_caixa',
|
||||
description: 'Consulta fluxo de caixa e previsões',
|
||||
endpoint: 'GET /api/soe/financeiro/fluxo'
|
||||
},
|
||||
{
|
||||
name: 'registrar_lancamento_contabil',
|
||||
description: 'Registra lançamento contábil manualmente',
|
||||
endpoint: 'POST /api/soe/contabil/lancamento'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. ESTRUTURA DE PASTAS A CRIAR
|
||||
|
||||
```
|
||||
server/
|
||||
└── soe/ ← Nova pasta (Central SOE)
|
||||
├── index.ts ← Router principal do SOE
|
||||
├── rule-engine.ts ← Motor de regras central
|
||||
│
|
||||
├── fiscal/
|
||||
│ ├── routes.ts ← GET/POST /api/soe/fiscal/*
|
||||
│ ├── rules.ts ← Regras fiscais (CFOP, NCM, tributação)
|
||||
│ ├── emissao.ts ← Orquestração de emissão (→ Plus)
|
||||
│ ├── plus-adapter.ts ← Adaptador para o Plus (invisível)
|
||||
│ └── sefaz-adapter.ts ← Comunicação direta SEFAZ (via Python 8002)
|
||||
│
|
||||
├── erp/
|
||||
│ ├── routes.ts ← GET/POST /api/soe/erp/*
|
||||
│ ├── rules.ts ← Regras de negócio ERP
|
||||
│ ├── frappe-adapter.ts ← Adaptador ERPNext (invisível)
|
||||
│ └── plus-erp-adapter.ts ← Adaptador Plus-ERP (para dados de venda)
|
||||
│
|
||||
├── contabil/
|
||||
│ ├── routes.ts ← GET/POST /api/soe/contabil/*
|
||||
│ ├── rules.ts ← Regras contábeis
|
||||
│ ├── auto-lancamentos.ts ← Lançamentos automáticos
|
||||
│ ├── plano-contas.ts ← Plano de contas SOE
|
||||
│ └── python-adapter.ts ← Adaptador Motor Python 8003
|
||||
│
|
||||
├── financeiro/
|
||||
│ ├── routes.ts ← GET/POST /api/soe/financeiro/*
|
||||
│ ├── rules.ts ← Regras financeiras
|
||||
│ ├── previsao.ts ← Previsão com IA
|
||||
│ └── conciliacao.ts ← Conciliação bancária
|
||||
│
|
||||
└── shared/
|
||||
├── types.ts ← Tipos SOE compartilhados
|
||||
├── audit.ts ← Registro de todas as operações
|
||||
└── event-bus.ts ← Eventos entre Centrais
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. TABELAS DO BANCO A CRIAR (Drizzle ORM)
|
||||
|
||||
```typescript
|
||||
// shared/schema.ts — Adicionar:
|
||||
|
||||
// Configuração da empresa no SOE
|
||||
export const soeEmpresaConfig = pgTable('soe_empresa_config', {
|
||||
id: serial('id').primaryKey(),
|
||||
empresaId: integer('empresa_id').notNull(),
|
||||
regimeTributario: varchar('regime_tributario', { length: 50 }), // simples, presumido, real
|
||||
uf: varchar('uf', { length: 2 }),
|
||||
cnpj: varchar('cnpj', { length: 18 }),
|
||||
planoContasId: integer('plano_contas_id'),
|
||||
configFiscal: jsonb('config_fiscal'), // alíquotas, CFOPs padrão
|
||||
configErp: jsonb('config_erp'), // qual ERP: plus, erpnext, ambos
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
})
|
||||
|
||||
// Regras configuráveis (editáveis pelo usuário)
|
||||
export const soeRegras = pgTable('soe_regras', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
empresaId: integer('empresa_id'), // null = regra global
|
||||
dominio: varchar('dominio', { length: 50 }).notNull(), // 'fiscal', 'erp', 'contabil', 'financeiro'
|
||||
nome: varchar('nome', { length: 200 }).notNull(),
|
||||
trigger: varchar('trigger', { length: 100 }).notNull(),
|
||||
condicao: jsonb('condicao').notNull(),
|
||||
acao: jsonb('acao').notNull(),
|
||||
prioridade: integer('prioridade').default(10),
|
||||
ativo: boolean('ativo').default(true),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
})
|
||||
|
||||
// Log de todas as operações SOE (audit trail)
|
||||
export const soeEventos = pgTable('soe_eventos', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
empresaId: integer('empresa_id').notNull(),
|
||||
dominio: varchar('dominio', { length: 50 }).notNull(),
|
||||
tipo: varchar('tipo', { length: 100 }).notNull(),
|
||||
regraIds: jsonb('regra_ids'), // regras aplicadas
|
||||
payloadEntrada: jsonb('payload_entrada'),
|
||||
payloadSaida: jsonb('payload_saida'),
|
||||
erp: varchar('erp', { length: 50 }), // 'plus', 'erpnext', 'python_8003', etc.
|
||||
status: varchar('status', { length: 50 }).notNull(),
|
||||
duracao: integer('duracao'), // ms
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
})
|
||||
|
||||
// Lançamentos contábeis centralizados
|
||||
export const soeLancamentos = pgTable('soe_lancamentos', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
empresaId: integer('empresa_id').notNull(),
|
||||
data: date('data').notNull(),
|
||||
contaDebito: varchar('conta_debito', { length: 30 }).notNull(),
|
||||
contaCredito: varchar('conta_credito', { length: 30 }).notNull(),
|
||||
valor: numeric('valor', { precision: 15, scale: 2 }).notNull(),
|
||||
historico: text('historico').notNull(),
|
||||
origem: varchar('origem', { length: 100 }), // 'emissao_nfe', 'pagamento', etc.
|
||||
origemId: varchar('origem_id', { length: 100 }),
|
||||
period: varchar('period', { length: 7 }), // '2026-03'
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. ROADMAP DE IMPLEMENTAÇÃO
|
||||
|
||||
### Fase 1 — Fundação (2–3 semanas)
|
||||
```
|
||||
[ ] 1. Criar server/soe/ com estrutura básica
|
||||
[ ] 2. Criar tabelas: soe_empresa_config, soe_regras, soe_eventos, soe_lancamentos
|
||||
[ ] 3. Implementar Plus Adapter (encapsula chamadas ao Laravel)
|
||||
[ ] 4. Implementar Frappe Adapter (encapsula chamadas ao ERPNext)
|
||||
[ ] 5. Endpoint GET /api/soe/status (health check de todos os motores)
|
||||
[ ] 6. Migrar /api/fisco/* para passar pelo SOE
|
||||
```
|
||||
|
||||
### Fase 2 — Central Fiscal (2–3 semanas)
|
||||
```
|
||||
[ ] 7. Implementar Rule Engine para fiscal
|
||||
[ ] 8. Criar regras fiscais base (NCM, CFOP, tributação por regime)
|
||||
[ ] 9. POST /api/soe/fiscal/emitir-nfe → Plus (invisível)
|
||||
[ ] 10. POST /api/soe/fiscal/cancelar-nfe → Plus (invisível)
|
||||
[ ] 11. GET /api/soe/fiscal/situacao → agrega Plus + Python 8002
|
||||
[ ] 12. Atualizar frontend /fisco para usar /api/soe/fiscal/*
|
||||
```
|
||||
|
||||
### Fase 3 — Central Contábil (2 semanas)
|
||||
```
|
||||
[ ] 13. Implementar auto-lancamentos para eventos fiscais
|
||||
[ ] 14. Integrar Motor Python 8003 via SOE
|
||||
[ ] 15. GET /api/soe/contabil/dre → resultado automático
|
||||
[ ] 16. GET /api/soe/contabil/balancete → atualizado em tempo real
|
||||
[ ] 17. Atualizar frontend /contabil para usar /api/soe/contabil/*
|
||||
```
|
||||
|
||||
### Fase 4 — Central Financeiro (2 semanas)
|
||||
```
|
||||
[ ] 18. Regras de boleto automático (via Asaas)
|
||||
[ ] 19. Alertas de vencimento (via Manus/WhatsApp)
|
||||
[ ] 20. Conciliação bancária semi-automática
|
||||
[ ] 21. Previsão financeira com IA (Motor Python 8003 + Manus)
|
||||
[ ] 22. Atualizar frontend /financeiro para usar /api/soe/financeiro/*
|
||||
```
|
||||
|
||||
### Fase 5 — Manus + SOE (1 semana)
|
||||
```
|
||||
[ ] 23. Registrar novas tools Manus apontando para SOE
|
||||
[ ] 24. Manus usa SOE para responder perguntas de negócio
|
||||
[ ] 25. Dashboard SOE: visão geral de todos os eventos
|
||||
```
|
||||
|
||||
### Fase 6 — Central ERP (3 semanas)
|
||||
```
|
||||
[ ] 26. Frappe Adapter completo (produtos, pedidos, estoque, compras)
|
||||
[ ] 27. Regras de negócio ERP no SOE
|
||||
[ ] 28. Sincronização bidirecional SOE ↔ ERPNext (via eventos)
|
||||
[ ] 29. Atualizar /erp para usar /api/soe/erp/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. RESULTADO FINAL — O QUE O USUÁRIO EXPERIMENTA
|
||||
|
||||
```
|
||||
Antes (fragmentado):
|
||||
Usuário → /plus → vê Laravel diretamente
|
||||
Usuário → /erp → vê ERPNext diretamente
|
||||
Regras fiscais só no Plus
|
||||
Contabilidade manual
|
||||
Financeiro sem automação
|
||||
|
||||
Depois (SOE Central):
|
||||
Usuário → /fisco → emite NF-e, Plus executa invisível
|
||||
Usuário → /contabil → vê DRE gerado automaticamente
|
||||
Usuário → /financeiro → vê fluxo, previsões, alertas automáticos
|
||||
Usuário → Manus → pergunta "qual meu resultado?" → resposta completa
|
||||
|
||||
Plus e ERPNext: INVISÍVEIS, executando em background
|
||||
Regras: CENTRALIZADAS no SOE, editáveis
|
||||
Lançamentos: AUTOMÁTICOS baseados em eventos (NF-e, pagamentos, etc.)
|
||||
Auditoria: TOTAL de todas as operações
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. PRINCÍPIO ORIENTADOR
|
||||
|
||||
> **"O usuário não sabe o que está por trás. Ele só sabe que funciona."**
|
||||
>
|
||||
> O Arcádia Suite é o **Sistema Operacional** da empresa.
|
||||
> O Plus e o ERPNext são **drivers de hardware** — poderosos, mas invisíveis.
|
||||
> O SOE Central é o **kernel** que os orquestra com regras e inteligência.
|
||||
|
||||
---
|
||||
|
||||
*PLANO_SOE_CENTRAL.md — Arcádia Suite v3.0*
|
||||
*Gerado em: 2026-03-16*
|
||||
|
|
@ -0,0 +1,666 @@
|
|||
# PLANO SOE CENTRAL v2 — Análise Real + Evolução
|
||||
## Baseado no MAPA_SOE_ARCADIA.md (Replit) + Auditoria do Código
|
||||
### Versão 2.0 — Março 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. DIAGNÓSTICO — O QUE JÁ EXISTE (e o plano v1 não sabia)
|
||||
|
||||
O MAPA_SOE_ARCADIA.md do Replit revelou que **o SOE já é uma realidade arquitetural robusta**, não apenas um conceito. Antes de planejar qualquer coisa, é crítico entender o que já funciona.
|
||||
|
||||
### 1.1 O que já está construído e funcionando
|
||||
|
||||
| Componente | Status | Localização |
|
||||
|---|---|---|
|
||||
| `SOE.tsx` | ✅ 2.414 linhas — UI completa com 9 tabs | `client/src/pages/SOE.tsx` |
|
||||
| `SoeMotorContext.tsx` | ✅ Seletor de motor (plus/erpnext) | `client/src/contexts/` |
|
||||
| `ErpApiClient` (interface) | ✅ Interface unificada definida | `server/erp/index.ts` |
|
||||
| `ArcadiaPlusClient` | ✅ Adaptador HTTP → Plus:8080 | `server/erp/index.ts` |
|
||||
| `ArcadiaNextClient` | ✅ Adaptador HTTP → ERPNext API | `server/erp/index.ts` |
|
||||
| `registerSoeRoutes()` | ✅ 60+ endpoints em /api/soe/* | `server/erp/routes.ts` |
|
||||
| `server/plus/proxy.ts` | ✅ Proxy reverso para Plus | `server/plus/` |
|
||||
| `server/plus/sso.ts` | ✅ SSO automático Plus ↔ Suite | `server/plus/` |
|
||||
| `server/plus/launcher.ts` | ✅ Lança PHP artisan serve | `server/plus/` |
|
||||
| Módulo de Pessoas unificado | ✅ `persons` + `person_roles` | Schema PostgreSQL |
|
||||
| Módulo de Produtos | ✅ Com NCM, CST, CFOP, IMEI | Schema PostgreSQL |
|
||||
| Módulo Financeiro | ✅ Contas a P/R + transações | Schema PostgreSQL |
|
||||
| Motor Fisco (:8002) | ✅ NF-e/NFC-e via nfelib | `server/python/` |
|
||||
| Motor Contábil (:8003) | ✅ DRE, Balanço, SPED | `server/python/` |
|
||||
| Sistema de módulos por tenant | ✅ `tenants.features` (JSONB) | Schema PostgreSQL |
|
||||
| Middleware de gating | ✅ `requireModule()` | `server/` |
|
||||
|
||||
### 1.2 Bancos de Dados por Motor (detalhe crítico)
|
||||
|
||||
```
|
||||
Arcádia Suite → PostgreSQL 16 (dados do SOE, pessoas, produtos, vendas...)
|
||||
Arcádia Plus → MySQL 8.0 (dados do Laravel — vendas, estoque, NF-e...)
|
||||
ERPNext → MariaDB (dados do Frappe — CRM, HR, projetos...)
|
||||
Motor Fisco → sem banco (só processa, retorna resultado)
|
||||
Motor Contábil → sem banco (só processa, retorna resultado)
|
||||
```
|
||||
|
||||
**Implicação:** Não há sincronização automática entre esses bancos. Um pedido criado no SOE (PostgreSQL) não aparece no Plus (MySQL) automaticamente — e vice-versa.
|
||||
|
||||
### 1.3 O padrão motor/adaptador atual
|
||||
|
||||
```
|
||||
Request do usuário
|
||||
│
|
||||
▼
|
||||
/api/soe/sales-orders (POST)
|
||||
│
|
||||
▼
|
||||
Verifica motor ativo (localStorage: arcadia_soe_motor)
|
||||
│
|
||||
├── motor = "local" → INSERT direto no PostgreSQL
|
||||
├── motor = "plus" → POST → Plus:8080 → MySQL
|
||||
└── motor = "erpnext" → POST → ERPNext API → MariaDB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. O PROBLEMA REAL — Onde o SOE atual para
|
||||
|
||||
O SOE atual é um **roteador inteligente**: recebe o request, decide para qual motor enviar, e despacha. Ele **não tem inteligência própria**.
|
||||
|
||||
### 2.1 O que falta (lacuna real)
|
||||
|
||||
```
|
||||
Situação atual:
|
||||
|
||||
SOE.tsx → /api/soe/* ────────────────────────────► Plus/ERPNext
|
||||
(sem regras, sem processamento)
|
||||
Regras ficam DENTRO do Plus
|
||||
Regras ficam DENTRO do ERPNext
|
||||
|
||||
Situação desejada:
|
||||
|
||||
SOE.tsx → /api/soe/* → [RULE ENGINE SOE] ──────► Plus/ERPNext
|
||||
│ (só executa)
|
||||
├── Regras Fiscais
|
||||
├── Regras Contábeis
|
||||
├── Regras Financeiras
|
||||
└── Regras de Negócio
|
||||
```
|
||||
|
||||
### 2.2 Problemas concretos hoje
|
||||
|
||||
| Problema | Impacto |
|
||||
|---|---|
|
||||
| Fiscal rules dentro do Plus | Se trocar de motor (Plus→ERPNext), perde todas as regras |
|
||||
| Nenhum lançamento contábil automático | DRE fica vazio, contabilidade manual |
|
||||
| Motor Contábil (:8003) existe mas nada alimenta ele | 0 lançamentos automáticos |
|
||||
| Plus tem UI própria (/plus) | Usuário pode bypassar o SOE e ir direto ao Plus |
|
||||
| Sem event bus entre motores | Uma NF-e emitida no Plus não dispara nada no SOE |
|
||||
| 25 conectores ERP definidos, zero chamadas reais | Integração existe no papel, não funciona |
|
||||
|
||||
### 2.3 O que o usuário vê hoje vs o que deveria ver
|
||||
|
||||
```
|
||||
Hoje:
|
||||
/soe → UI do SOE (tabs: dashboard, pessoas, produtos, vendas...)
|
||||
/plus → UI do Plus (Laravel) — direto, sem passar pelo SOE
|
||||
/erp → UI do ERP legado
|
||||
(usuário VÊ o Plus se quiser)
|
||||
|
||||
Desejado:
|
||||
/soe → UI do SOE (mesma, mas com inteligência)
|
||||
/plus → BLOQUEADO ou redirecionado para /soe
|
||||
/erp → BLOQUEADO ou redirecionado para /soe
|
||||
(Plus e ERPNext = invisíveis, executando em background)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. ARQUITETURA EVOLUÍDA — SOE com Rule Engine
|
||||
|
||||
### 3.1 O que muda (e o que fica igual)
|
||||
|
||||
**NÃO MUDA:**
|
||||
- `SOE.tsx` — a UI continua a mesma (só ganha funcionalidades)
|
||||
- `SoeMotorContext.tsx` — seleção de motor continua existindo
|
||||
- `ArcadiaPlusClient` / `ArcadiaNextClient` — adaptadores continuam
|
||||
- Todas as rotas `/api/soe/*` — continuam funcionando
|
||||
- Todos os bancos — PostgreSQL, MySQL, MariaDB — continuam separados
|
||||
|
||||
**MUDA / ADICIONA:**
|
||||
- Uma **camada de Rule Engine** entre as rotas e os adaptadores
|
||||
- **Event Bus SOE** que dispara lançamentos contábeis automaticamente
|
||||
- **Ocultação do Plus** — `/plus` passa a ser controlado pelo SOE
|
||||
- **Regras fiscais** saem do Plus e entram no SOE
|
||||
|
||||
### 3.2 Diagrama completo com Rule Engine
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ ARCÁDIA SUITE — CAMADA DE APRESENTAÇÃO │
|
||||
│ │
|
||||
│ SOE.tsx (2.414+ linhas) │
|
||||
│ Tabs: Dashboard | Pessoas | Produtos | Vendas | Compras │
|
||||
│ Financeiro | CRM | Sincronização | Config │
|
||||
│ │
|
||||
│ SoeMotorContext.tsx → motor = "plus" | "erpnext" | "local" │
|
||||
└───────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ /api/soe/* — CAMADA DE API (já existe) │
|
||||
│ registerSoeRoutes() em server/erp/routes.ts │
|
||||
│ requireAuth + requireModule middleware │
|
||||
└───────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼ ◄── INSERIR AQUI (novo)
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ SOE RULE ENGINE (NOVO) │
|
||||
│ server/soe/rule-engine/ │
|
||||
│ │
|
||||
│ Antes de despachar para o motor, o Rule Engine: │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 1. FISCAL RULES (server/soe/rule-engine/fiscal.ts) │ │
|
||||
│ │ • Resolve CFOP correto (intra/interestadual) │ │
|
||||
│ │ • Aplica tributação por regime (Simples/Presumido/Real) │ │
|
||||
│ │ • Valida NCM obrigatório │ │
|
||||
│ │ • Define CST/CSOSN automático │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 2. BUSINESS RULES (server/soe/rule-engine/business.ts) │ │
|
||||
│ │ • Aprovação automática de pedidos < R$ X │ │
|
||||
│ │ • Reserva de estoque ao confirmar venda │ │
|
||||
│ │ • Trigger de NF-e ao faturar pedido │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 3. ACCOUNTING RULES (server/soe/rule-engine/accounting.ts) │ │
|
||||
│ │ • Emitiu NF-e → lançamento automático D: Clientes │ │
|
||||
│ │ C: Receita │ │
|
||||
│ │ • Pagou conta → lançamento automático D: Fornecedor │ │
|
||||
│ │ C: Banco │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ DESPACHADOR DE MOTOR (já existe, aprimorado) │
|
||||
│ createErpClient(connection) em server/erp/index.ts │
|
||||
│ │
|
||||
│ motor = "local" → INSERT PostgreSQL │
|
||||
│ motor = "plus" → ArcadiaPlusClient → Plus:8080 → MySQL │
|
||||
│ motor = "erpnext" → ArcadiaNextClient → ERPNext API → MariaDB │
|
||||
└───────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ SOE EVENT BUS (NOVO) │
|
||||
│ server/soe/event-bus.ts │
|
||||
│ │
|
||||
│ Depois que o motor executa, o Event Bus dispara: │
|
||||
│ │
|
||||
│ evento: "nfe_emitida" → Motor Contábil (lançamento auto) │
|
||||
│ evento: "venda_confirmada" → Reserva estoque + alerta WhatsApp │
|
||||
│ evento: "pagamento_feito" → Baixa C/P + lançamento contábil │
|
||||
│ evento: "recebimento" → Baixa C/R + lançamento contábil │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. O QUE CONSTRUIR — Detalhamento por Módulo
|
||||
|
||||
### 4.1 SOE Rule Engine (PRIORIDADE MÁXIMA)
|
||||
|
||||
**Localização:** `server/soe/rule-engine/`
|
||||
|
||||
Este é o coração da evolução. É um middleware que intercepta as chamadas às rotas SOE **antes** de despachar para o motor.
|
||||
|
||||
#### Como funciona na prática
|
||||
|
||||
```typescript
|
||||
// server/erp/routes.ts — modificação cirúrgica
|
||||
|
||||
// ANTES (atual):
|
||||
app.post("/api/soe/sales-orders", requireAuth, async (req, res) => {
|
||||
const client = getActiveClient(req) // pega Plus ou ERPNext
|
||||
const result = await client.createSalesOrder(req.body) // despacha direto
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
// DEPOIS (com Rule Engine):
|
||||
app.post("/api/soe/sales-orders", requireAuth, async (req, res) => {
|
||||
// 1. Aplica regras ANTES de despachar
|
||||
const enriched = await ruleEngine.apply('pre_sales_order', req.body, req.user)
|
||||
|
||||
// 2. Despacha para o motor com dados enriquecidos
|
||||
const client = getActiveClient(req)
|
||||
const result = await client.createSalesOrder(enriched.payload)
|
||||
|
||||
// 3. Dispara eventos DEPOIS da execução
|
||||
await eventBus.emit('sales_order_created', { result, rules: enriched.appliedRules })
|
||||
|
||||
res.json(result)
|
||||
})
|
||||
```
|
||||
|
||||
#### Estrutura do Rule Engine
|
||||
|
||||
```typescript
|
||||
// server/soe/rule-engine/index.ts
|
||||
|
||||
interface SoeRule {
|
||||
id: string
|
||||
dominio: 'fiscal' | 'business' | 'accounting' | 'financial'
|
||||
trigger: string // ex: 'pre_sales_order', 'post_nfe_emitida'
|
||||
condicao: RuleCondition // quando esta regra se aplica
|
||||
acao: RuleAction // o que ela faz ao payload
|
||||
prioridade: number
|
||||
ativo: boolean
|
||||
empresaId?: number // null = regra global de todos os tenants
|
||||
}
|
||||
|
||||
class SoeRuleEngine {
|
||||
async apply(trigger: string, payload: any, context: RuleContext): Promise<EnrichedPayload> {
|
||||
// 1. Busca regras ativas para este trigger (do banco + defaults hardcoded)
|
||||
const rules = await this.getActiveRules(trigger, context.tenantId)
|
||||
|
||||
// 2. Filtra regras cuja condição se aplica ao payload
|
||||
const applicable = rules.filter(r => this.avaliarCondicao(r.condicao, payload, context))
|
||||
|
||||
// 3. Aplica cada regra em ordem de prioridade
|
||||
let enriched = { ...payload }
|
||||
const applied: string[] = []
|
||||
for (const rule of applicable.sort((a, b) => a.prioridade - b.prioridade)) {
|
||||
enriched = await this.aplicarAcao(rule.acao, enriched, context)
|
||||
applied.push(rule.id)
|
||||
}
|
||||
|
||||
return { payload: enriched, appliedRules: applied }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Regras Fiscais no SOE
|
||||
|
||||
Estas regras **hoje vivem dentro do Plus** (Laravel controllers). A ideia é trazê-las para o SOE.
|
||||
|
||||
**Nota importante:** Não é mover o código PHP para TypeScript. É criar regras **declarativas** no SOE que enriquecem o payload **antes** de enviar ao Plus. O Plus ainda processa e emite — mas recebe o payload já correto.
|
||||
|
||||
```typescript
|
||||
// server/soe/rule-engine/fiscal-rules.ts
|
||||
|
||||
export const FISCAL_RULES_PADRAO: SoeRule[] = [
|
||||
|
||||
// CFOP — resolve automaticamente por operação
|
||||
{
|
||||
id: 'CFOP_VENDA_DENTRO_ESTADO',
|
||||
dominio: 'fiscal',
|
||||
trigger: 'pre_sales_order',
|
||||
condicao: { uf_emitente: '==', uf_destinatario: true, tipo_op: 'venda_produto' },
|
||||
acao: { set: { 'items[*].cfop': '5.102' } },
|
||||
prioridade: 10, ativo: true
|
||||
},
|
||||
{
|
||||
id: 'CFOP_VENDA_OUTRO_ESTADO',
|
||||
dominio: 'fiscal',
|
||||
trigger: 'pre_sales_order',
|
||||
condicao: { uf_emitente: '!=', uf_destinatario: true, tipo_op: 'venda_produto' },
|
||||
acao: { set: { 'items[*].cfop': '6.102' } },
|
||||
prioridade: 10, ativo: true
|
||||
},
|
||||
|
||||
// Tributação por Regime
|
||||
{
|
||||
id: 'CST_SIMPLES_NACIONAL',
|
||||
dominio: 'fiscal',
|
||||
trigger: 'pre_nfe_emissao',
|
||||
condicao: { regime_tributario: 'simples_nacional' },
|
||||
acao: {
|
||||
set: { 'items[*].cst_icms': '400', 'items[*].pis': null, 'items[*].cofins': null },
|
||||
// No Simples Nacional: CSOSN 400 = tributado, sem destaque PIS/COFINS
|
||||
},
|
||||
prioridade: 5, ativo: true
|
||||
},
|
||||
{
|
||||
id: 'CST_LUCRO_PRESUMIDO',
|
||||
dominio: 'fiscal',
|
||||
trigger: 'pre_nfe_emissao',
|
||||
condicao: { regime_tributario: 'lucro_presumido' },
|
||||
acao: { set: { 'items[*].cst_pis': '01', 'items[*].cst_cofins': '01' } },
|
||||
prioridade: 5, ativo: true
|
||||
},
|
||||
|
||||
// Validações obrigatórias
|
||||
{
|
||||
id: 'VALIDAR_NCM_NFE',
|
||||
dominio: 'fiscal',
|
||||
trigger: 'pre_nfe_emissao',
|
||||
condicao: { tipo_documento: ['nfe', 'nfce'] },
|
||||
acao: { validar: { campo: 'items[*].ncm', obrigatorio: true, formato: /^\d{8}$/ } },
|
||||
prioridade: 1, ativo: true
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### 4.3 SOE Event Bus — Lançamentos Contábeis Automáticos
|
||||
|
||||
Este é o **maior salto de valor**. Hoje o Motor Contábil (:8003) existe mas nada o alimenta automaticamente.
|
||||
|
||||
```typescript
|
||||
// server/soe/event-bus.ts
|
||||
|
||||
type SoeEvent =
|
||||
| 'nfe_emitida'
|
||||
| 'nfe_cancelada'
|
||||
| 'venda_confirmada'
|
||||
| 'compra_recebida'
|
||||
| 'pagamento_realizado'
|
||||
| 'recebimento_realizado'
|
||||
| 'boleto_gerado'
|
||||
|
||||
interface SoeEventPayload {
|
||||
empresaId: number
|
||||
tenantId: number
|
||||
evento: SoeEvent
|
||||
dados: Record<string, any>
|
||||
motorOrigem: 'plus' | 'erpnext' | 'local'
|
||||
}
|
||||
|
||||
class SoeEventBus {
|
||||
async emit(evento: SoeEvent, payload: SoeEventPayload) {
|
||||
// Registra no banco (audit trail)
|
||||
await db.insert(soeEventos).values({ ...payload, createdAt: new Date() })
|
||||
|
||||
// Dispara handlers
|
||||
await Promise.allSettled(
|
||||
this.getHandlers(evento).map(h => h(payload))
|
||||
)
|
||||
}
|
||||
|
||||
// Handlers por evento
|
||||
private handlers: Record<SoeEvent, EventHandler[]> = {
|
||||
'nfe_emitida': [
|
||||
contabilHandler.lancamentoVenda, // D: Clientes / C: Receita Bruta
|
||||
contabilHandler.lancamentoImpostos, // D: Impostos / C: ICMS/PIS/COFINS a Recolher
|
||||
financialHandler.gerarRecebivel, // Cria C/R automaticamente
|
||||
],
|
||||
'pagamento_realizado': [
|
||||
contabilHandler.lancamentoPagamento, // D: Fornecedores / C: Banco
|
||||
financialHandler.baixarContaPagar, // Baixa C/P
|
||||
],
|
||||
'recebimento_realizado': [
|
||||
contabilHandler.lancamentoRecebimento, // D: Banco / C: Clientes
|
||||
financialHandler.baixarContaReceber, // Baixa C/R
|
||||
],
|
||||
'compra_recebida': [
|
||||
contabilHandler.lancamentoCompra, // D: Estoque / C: Fornecedores
|
||||
],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Lançamentos automáticos para cada evento
|
||||
|
||||
```
|
||||
NF-e emitida (venda):
|
||||
D: 1.1.3.01 — Clientes (valor total da nota)
|
||||
C: 3.1.1.01 — Receita Bruta de Vendas
|
||||
|
||||
D: 3.1.2.01 — Deduções (ICMS)
|
||||
C: 2.1.2.01 — ICMS a Recolher
|
||||
|
||||
D: 3.1.2.02 — Deduções (PIS)
|
||||
C: 2.1.2.02 — PIS a Recolher
|
||||
|
||||
D: 3.1.2.03 — Deduções (COFINS)
|
||||
C: 2.1.2.03 — COFINS a Recolher
|
||||
|
||||
Pagamento de fornecedor:
|
||||
D: 2.1.1.{categoria} — Fornecedores
|
||||
C: 1.1.1.01 — Banco (conta configurada)
|
||||
|
||||
Recebimento de cliente:
|
||||
D: 1.1.1.01 — Banco
|
||||
C: 1.1.3.01 — Clientes
|
||||
|
||||
Compra recebida (NF-e entrada):
|
||||
D: 1.1.4.01 — Estoque / Mercadorias
|
||||
C: 2.1.1.01 — Fornecedores
|
||||
```
|
||||
|
||||
### 4.4 Ocultação do Plus e ERPNext
|
||||
|
||||
O Plus tem página própria em `/plus` (client/src/pages/Plus.tsx). O usuário pode acessá-la diretamente. Precisamos de 3 mudanças:
|
||||
|
||||
#### a) Redirecionar `/plus` para `/soe`
|
||||
|
||||
```typescript
|
||||
// client/src/App.tsx — modificação simples
|
||||
// Trocar: <Route path="/plus" component={Plus} />
|
||||
// Por: <Route path="/plus" render={() => <Redirect to="/soe" />} />
|
||||
```
|
||||
|
||||
**Mas manter o proxy reverso Plus ativo** — o Plus continua funcionando em background, só não fica visível como página separada.
|
||||
|
||||
#### b) Ocultar o seletor de motor do usuário final
|
||||
|
||||
```typescript
|
||||
// client/src/contexts/SoeMotorContext.tsx
|
||||
// O seletor fica SOMENTE na tab Config do SOE, e somente para admins
|
||||
|
||||
// A tab Config na SOE.tsx — adicionar role check:
|
||||
{user?.role === 'admin' && <TabsTrigger value="config">Config</TabsTrigger>}
|
||||
```
|
||||
|
||||
#### c) Na tab Config (admin), o label muda
|
||||
|
||||
```
|
||||
Antes: "Selecione o motor: Plus | ERPNext"
|
||||
Depois: "Motor de execução: Plus | ERPNext | Local"
|
||||
(invisível para usuário final, só admin de tenant vê)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. TABELAS A CRIAR (complemento ao schema existente)
|
||||
|
||||
As tabelas canônicas do SOE já existem. Precisamos adicionar apenas as tabelas do Rule Engine.
|
||||
|
||||
```typescript
|
||||
// shared/schema.ts — adicionar ao final
|
||||
|
||||
// Regras do SOE — configuráveis por tenant
|
||||
export const soeRegras = pgTable('soe_regras', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
tenantId: integer('tenant_id'), // null = regra global (todas as empresas)
|
||||
empresaId: integer('empresa_id'), // null = todas as empresas do tenant
|
||||
dominio: varchar('dominio', { length: 50 }).notNull(),
|
||||
trigger: varchar('trigger', { length: 100 }).notNull(),
|
||||
nome: varchar('nome', { length: 200 }).notNull(),
|
||||
condicao: jsonb('condicao').notNull(),
|
||||
acao: jsonb('acao').notNull(),
|
||||
prioridade: integer('prioridade').default(10),
|
||||
ativo: boolean('ativo').default(true),
|
||||
origemPadrao: boolean('origem_padrao').default(false), // true = regra default do sistema
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow(),
|
||||
})
|
||||
|
||||
// Log de todos os eventos SOE (audit trail completo)
|
||||
export const soeEventos = pgTable('soe_eventos', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
tenantId: integer('tenant_id').notNull(),
|
||||
empresaId: integer('empresa_id'),
|
||||
evento: varchar('evento', { length: 100 }).notNull(),
|
||||
motorOrigem: varchar('motor_origem', { length: 50 }), // 'plus', 'erpnext', 'local'
|
||||
regraIds: jsonb('regra_ids'), // regras aplicadas
|
||||
payloadEntrada: jsonb('payload_entrada'),
|
||||
payloadSaida: jsonb('payload_saida'),
|
||||
status: varchar('status', { length: 50 }).notNull(),
|
||||
duracaoMs: integer('duracao_ms'),
|
||||
erro: text('erro'),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
})
|
||||
|
||||
// Lançamentos contábeis automáticos do SOE
|
||||
export const soeLancamentos = pgTable('soe_lancamentos', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
tenantId: integer('tenant_id').notNull(),
|
||||
empresaId: integer('empresa_id').notNull(),
|
||||
data: date('data').notNull(),
|
||||
contaDebito: varchar('conta_debito', { length: 30 }).notNull(),
|
||||
contaCredito: varchar('conta_credito', { length: 30 }).notNull(),
|
||||
valor: numeric('valor', { precision: 15, scale: 2 }).notNull(),
|
||||
historico: text('historico').notNull(),
|
||||
origemEvento: varchar('origem_evento', { length: 100 }), // 'nfe_emitida', etc.
|
||||
origemEventoId: uuid('origem_evento_id'), // FK para soe_eventos
|
||||
periodo: varchar('periodo', { length: 7 }), // '2026-03'
|
||||
enviado8003: boolean('enviado_8003').default(false), // sincronizado com Motor Contábil
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. ESTRUTURA DE PASTAS (Novo código a criar)
|
||||
|
||||
```
|
||||
server/
|
||||
└── soe/ ← NOVA PASTA
|
||||
├── rule-engine/
|
||||
│ ├── index.ts ← SoeRuleEngine class
|
||||
│ ├── fiscal-rules.ts ← Regras fiscais padrão
|
||||
│ ├── business-rules.ts ← Regras de negócio
|
||||
│ └── accounting-rules.ts ← Regras de lançamento contábil
|
||||
│
|
||||
├── event-bus.ts ← SoeEventBus class
|
||||
│
|
||||
├── handlers/
|
||||
│ ├── contabil-handler.ts ← Lançamentos automáticos
|
||||
│ └── financial-handler.ts ← Automação financeira
|
||||
│
|
||||
└── fiscal/
|
||||
└── cfop-resolver.ts ← Resolução de CFOP (lógica complexa)
|
||||
```
|
||||
|
||||
**Modificações em arquivos existentes:**
|
||||
```
|
||||
server/erp/routes.ts ← Inserir ruleEngine.apply() antes do dispatch
|
||||
client/src/App.tsx ← Redirecionar /plus para /soe
|
||||
client/src/pages/SOE.tsx ← Ocultar Config tab para não-admins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. ROADMAP — Fases de Implementação
|
||||
|
||||
### Fase 1 — Rule Engine Básico (1 semana)
|
||||
```
|
||||
[ ] Criar server/soe/rule-engine/index.ts (SoeRuleEngine)
|
||||
[ ] Criar tabelas soe_regras e soe_eventos (drizzle push)
|
||||
[ ] Inserir ruleEngine.apply() em 3 rotas críticas:
|
||||
- POST /api/soe/sales-orders
|
||||
- POST /api/soe/sales-orders/:id/generate-nfe
|
||||
- POST /api/soe/purchase-orders
|
||||
[ ] Seed das regras fiscais padrão (CFOP, CST, validações NCM)
|
||||
[ ] Testar: criar pedido → verificar que payload está sendo enriquecido
|
||||
```
|
||||
|
||||
### Fase 2 — Event Bus + Contabilidade Automática (1 semana)
|
||||
```
|
||||
[ ] Criar server/soe/event-bus.ts
|
||||
[ ] Criar tabela soe_lancamentos (drizzle push)
|
||||
[ ] Criar handlers: contabil-handler.ts + financial-handler.ts
|
||||
[ ] Conectar: POST generate-nfe → emit('nfe_emitida') → lançamentos
|
||||
[ ] Conectar: pagamento/recebimento → emit() → lançamentos
|
||||
[ ] Criar endpoint: GET /api/soe/contabil/lancamentos?periodo=YYYY-MM
|
||||
[ ] DRE: agora tem dados reais (via soe_lancamentos → Motor :8003)
|
||||
```
|
||||
|
||||
### Fase 3 — Ocultar Plus/ERPNext (3 dias)
|
||||
```
|
||||
[ ] client/src/App.tsx: /plus → redirect para /soe
|
||||
[ ] client/src/pages/SOE.tsx: Config tab só para admins
|
||||
[ ] Manter proxy Plus funcionando (invisível)
|
||||
[ ] Testar: fluxo completo sem nenhum acesso direto ao Plus UI
|
||||
```
|
||||
|
||||
### Fase 4 — Regras Configuráveis pelo Usuário (1 semana)
|
||||
```
|
||||
[ ] UI na tab Config do SOE: "Regras de Negócio"
|
||||
[ ] CRUD de regras via /api/soe/rules
|
||||
[ ] Admin pode criar regras custom sem código
|
||||
[ ] Exemplo: "Pedidos acima de R$10.000 requerem aprovação manual"
|
||||
```
|
||||
|
||||
### Fase 5 — Financeiro Avançado (2 semanas)
|
||||
```
|
||||
[ ] Auto-boleto ao confirmar venda (Asaas API)
|
||||
[ ] Alertas de vencimento (Manus → WhatsApp)
|
||||
[ ] Conciliação bancária OFX semi-automática
|
||||
[ ] Previsão financeira (Motor BI :8004 + histórico soe_lancamentos)
|
||||
```
|
||||
|
||||
### Fase 6 — Contábil Avançado (2 semanas)
|
||||
```
|
||||
[ ] DRE automático mensal (100% alimentado pelo Event Bus)
|
||||
[ ] Balanço Patrimonial automático
|
||||
[ ] Fechamento contábil mensal
|
||||
[ ] SPED ECD/ECF alimentado por soe_lancamentos
|
||||
[ ] Exportação para escritório contábil (OFX, Excel, PDF)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. COMPARAÇÃO: PLANO v1 vs PLANO v2
|
||||
|
||||
| Aspecto | Plano v1 (antes do Replit .md) | Plano v2 (com conhecimento real) |
|
||||
|---|---|---|
|
||||
| Adaptadores Plus/ERPNext | "Precisam ser criados" | Já existem — não tocar |
|
||||
| SOE Routes | "Precisam ser criadas" | 60+ endpoints já existem |
|
||||
| SOE.tsx | "Precisa ser construída" | 2.414 linhas — já funciona |
|
||||
| Onde adicionar código | server/soe/ do zero | Middleware nas rotas existentes |
|
||||
| Esforço estimado | Alto (refatoração total) | Médio (adição cirúrgica) |
|
||||
| Risco de quebrar | Alto | Baixo (adição, não substituição) |
|
||||
| Banco Plus | Ignorado | MySQL — separado, nunca tocar direto |
|
||||
|
||||
---
|
||||
|
||||
## 9. PRINCÍPIO ORIENTADOR (REVISADO)
|
||||
|
||||
> O SOE já é o roteador certo. Agora vamos torná-lo o **árbitro de regras**.
|
||||
>
|
||||
> O padrão motor/adaptador está perfeito. O que falta é a **inteligência entre a rota e o adaptador**.
|
||||
>
|
||||
> Plus e ERPNext continuam existindo e funcionando — só que o usuário final não sabe.
|
||||
> Ele só vê a Central SOE. O restante é infraestrutura.
|
||||
|
||||
---
|
||||
|
||||
## 10. RESUMO EXECUTIVO — O que fazer agora
|
||||
|
||||
**1 ação que entrega valor imediato:**
|
||||
```
|
||||
Criar o SoeRuleEngine e plugar nas rotas de venda/NF-e.
|
||||
Resultado: Ao emitir uma NF-e, o CFOP correto é calculado
|
||||
automaticamente pelo SOE — sem o usuário saber que o Plus está
|
||||
sendo chamado por baixo.
|
||||
```
|
||||
|
||||
**1 ação que entrega valor a médio prazo:**
|
||||
```
|
||||
Criar o Event Bus com o handler de contabilidade.
|
||||
Resultado: Cada NF-e emitida gera lançamentos contábeis
|
||||
automaticamente. O DRE do mês é gerado sem entrada manual de dados.
|
||||
```
|
||||
|
||||
**1 ação que fecha o conceito:**
|
||||
```
|
||||
Redirecionar /plus → /soe.
|
||||
Resultado: O usuário não tem mais acesso ao Plus diretamente.
|
||||
Ele só vê o Arcádia. O Plus é infraestrutura invisível.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*PLANO_SOE_CENTRAL_v2.md — Arcádia Suite v3.0*
|
||||
*Gerado em: 2026-03-16 — Baseado na análise real do MAPA_SOE_ARCADIA.md (Replit)*
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
|
@ -9,6 +9,7 @@ import { SoeMotorProvider } from "@/contexts/SoeMotorContext";
|
|||
import { ProtectedRoute } from "@/lib/protected-route";
|
||||
import { CommandPalette } from "@/components/CommandPalette";
|
||||
import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit";
|
||||
import { OpenClawWidget } from "@/components/openclaw/OpenClawWidget";
|
||||
import NotFound from "@/pages/not-found";
|
||||
import AuthPage from "@/pages/auth-page";
|
||||
|
||||
|
|
@ -70,8 +71,6 @@ const XosPipeline = lazy(() => import("@/pages/XosPipeline"));
|
|||
const XosSupervisor = lazy(() => import("@/pages/XosSupervisor"));
|
||||
const XosReports = lazy(() => import("@/pages/XosReports"));
|
||||
const XosProtocols = lazy(() => import("@/pages/XosProtocols"));
|
||||
const Skills = lazy(() => import("@/pages/Skills"));
|
||||
const AutomationCenter = lazy(() => import("@/pages/AutomationCenter"));
|
||||
|
||||
|
||||
function LoadingFallback() {
|
||||
|
|
@ -148,8 +147,6 @@ function Router() {
|
|||
<ProtectedRoute path="/page-builder" component={PageBuilder} />
|
||||
<ProtectedRoute path="/migration" component={Migration} />
|
||||
<ProtectedRoute path="/dev-center" component={DevCenter} />
|
||||
<ProtectedRoute path="/skills" component={Skills} />
|
||||
<ProtectedRoute path="/automations-center" component={AutomationCenter} />
|
||||
<ProtectedRoute path="/page/:id" component={WorkspacePage} />
|
||||
<ProtectedRoute path="/app/:id" component={AppViewer} />
|
||||
<Route path="/auth" component={AuthPage} />
|
||||
|
|
@ -168,6 +165,7 @@ function App() {
|
|||
<KnowledgeCollectorInit />
|
||||
<Toaster />
|
||||
<CommandPalette />
|
||||
<OpenClawWidget />
|
||||
<Router />
|
||||
</TooltipProvider>
|
||||
</SoeMotorProvider>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useLocation } from "wouter";
|
||||
import React, { useEffect } from "react";
|
||||
import { Bot, Settings, MessageCircle, Zap, LayoutDashboard, Compass, Users, Ticket, LogOut, User, Shield, Receipt, Package, Rocket, Beaker, TrendingUp, MapPin, Droplets, Star, Database, Layout, Code, Code2, Store, Layers, Building2 } from "lucide-react";
|
||||
const browserIcon = "/arcadia_suite_icon.png";
|
||||
import { Bot, Settings, MessageCircle, Zap, LayoutDashboard, Compass, Users, Ticket, LogOut, User, Shield, Receipt, Package, Rocket, Beaker, TrendingUp, MapPin, Droplets, Star, Database, Layout, Code, Code2, Store, Layers } from "lucide-react";
|
||||
import browserIcon from "@assets/arcadia_branding/arcadia_suite_icon.png";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNavigationTracking } from "@/hooks/use-navigation-tracking";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -31,7 +31,7 @@ function CompactNavigationBar() {
|
|||
return (
|
||||
<div className="h-10 bg-background border-b border-border flex items-center px-3 gap-2 text-xs text-muted-foreground shadow-xs z-10">
|
||||
<div className="flex items-center gap-1 overflow-x-auto scrollbar-hide flex-1">
|
||||
{(user?.role === "admin" || user?.role === "master") && (
|
||||
{user?.role === "admin" && (
|
||||
<div
|
||||
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
|
||||
onClick={() => navigateTo("/admin", "Administração")}
|
||||
|
|
@ -123,13 +123,13 @@ function CompactNavigationBar() {
|
|||
</div>
|
||||
<div
|
||||
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
|
||||
onClick={() => navigateTo("/soe", "SOE")}
|
||||
data-testid="bookmark-soe"
|
||||
onClick={() => navigateTo("/erp", "ERP")}
|
||||
data-testid="bookmark-erp"
|
||||
>
|
||||
<div className="w-4 h-4 bg-gradient-to-br from-blue-600 to-blue-800 rounded-sm flex items-center justify-center">
|
||||
<Building2 className="w-2.5 h-2.5 text-white" />
|
||||
<Package className="w-2.5 h-2.5 text-white" />
|
||||
</div>
|
||||
<span className="hidden md:inline">SOE</span>
|
||||
<span className="hidden md:inline">ERP</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
|
||||
|
|
@ -141,7 +141,16 @@ function CompactNavigationBar() {
|
|||
</div>
|
||||
<span className="hidden md:inline">Retail</span>
|
||||
</div>
|
||||
{/* Plus hidden temporarily */}
|
||||
<div
|
||||
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
|
||||
onClick={() => navigateTo("/plus", "Plus")}
|
||||
data-testid="bookmark-plus"
|
||||
>
|
||||
<div className="w-4 h-4 bg-gradient-to-br from-purple-500 to-purple-700 rounded-sm flex items-center justify-center">
|
||||
<Layers className="w-2.5 h-2.5 text-white" />
|
||||
</div>
|
||||
<span className="hidden md:inline">Plus</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-1 hover:bg-muted px-2 py-1.5 rounded cursor-pointer transition-colors flex-shrink-0"
|
||||
onClick={() => navigateTo("/fisco", "Fisco")}
|
||||
|
|
@ -179,10 +188,10 @@ function CompactNavigationBar() {
|
|||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{user?.name || user?.username}</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">@{user?.username}</p>
|
||||
{(user?.role === "admin" || user?.role === "master") && (
|
||||
{user?.role === "admin" && (
|
||||
<p className="text-xs leading-none text-primary flex items-center gap-1 mt-1">
|
||||
<Shield className="w-3 h-3" />
|
||||
{user?.role === "master" ? "Administrador Master" : "Administrador"}
|
||||
Administrador
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -192,7 +201,7 @@ function CompactNavigationBar() {
|
|||
<User className="mr-2 h-4 w-4" />
|
||||
<span>Meu Perfil</span>
|
||||
</DropdownMenuItem>
|
||||
{(user?.role === "admin" || user?.role === "master") && (
|
||||
{user?.role === "admin" && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">Plataforma</DropdownMenuLabel>
|
||||
|
|
|
|||
|
|
@ -80,10 +80,10 @@ export function Omnibox({ url, onNavigate, isLoading }: OmniboxProps) {
|
|||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{user?.name || user?.username}</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">@{user?.username}</p>
|
||||
{(user?.role === "admin" || user?.role === "master") && (
|
||||
{user?.role === "admin" && (
|
||||
<p className="text-xs leading-none text-primary flex items-center gap-1 mt-1">
|
||||
<Shield className="w-3 h-3" />
|
||||
{user?.role === "master" ? "Administrador Master" : "Administrador"}
|
||||
Administrador
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -121,10 +121,10 @@ export function CommandPalette() {
|
|||
},
|
||||
{
|
||||
id: "nav-crm",
|
||||
name: "Abrir Manager Partners",
|
||||
name: "Abrir Arcádia CRM",
|
||||
icon: <Handshake className="h-4 w-4" />,
|
||||
action: () => navigateTo("/crm"),
|
||||
keywords: ["crm", "parceiros", "contratos", "manager", "partners"],
|
||||
keywords: ["crm", "parceiros", "contratos", "vendas", "whatsapp"],
|
||||
group: "Navegação",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ export function SupersetDashboard({
|
|||
const [status, setStatus] = useState<"loading" | "ready" | "error" | "unavailable">("loading");
|
||||
const [errorMsg, setErrorMsg] = useState<string>("");
|
||||
|
||||
// Busca token e embeddedId (UUID) do backend
|
||||
const fetchTokenData = useCallback(async (): Promise<{ token: string; embeddedId: string }> => {
|
||||
const fetchGuestToken = useCallback(async (): Promise<string> => {
|
||||
const resp = await fetch("/api/superset/guest-token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -39,7 +38,8 @@ export function SupersetDashboard({
|
|||
const err = await resp.json().catch(() => ({ error: "Erro desconhecido" }));
|
||||
throw new Error(err.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
return resp.json();
|
||||
const { token } = await resp.json();
|
||||
return token;
|
||||
}, [dashboardId]);
|
||||
|
||||
const loadDashboard = useCallback(async () => {
|
||||
|
|
@ -56,9 +56,6 @@ export function SupersetDashboard({
|
|||
return;
|
||||
}
|
||||
|
||||
// Resolve embeddedId (UUID) e obtém token inicial
|
||||
const { embeddedId } = await fetchTokenData();
|
||||
|
||||
// Importa SDK dinamicamente (só quando necessário)
|
||||
const { embedDashboard } = await import("@superset-ui/embedded-sdk");
|
||||
|
||||
|
|
@ -68,13 +65,10 @@ export function SupersetDashboard({
|
|||
}
|
||||
|
||||
sdkRef.current = await embedDashboard({
|
||||
id: embeddedId, // UUID do embedded config (não o slug)
|
||||
supersetDomain: "https://bi.onboardbi.com.br",
|
||||
id: dashboardId,
|
||||
supersetDomain: window.location.origin + "/superset",
|
||||
mountPoint: containerRef.current,
|
||||
fetchGuestToken: async () => {
|
||||
const { token } = await fetchTokenData();
|
||||
return token;
|
||||
},
|
||||
fetchGuestToken,
|
||||
dashboardUiConfig: {
|
||||
hideTitle: true,
|
||||
hideChartControls: false,
|
||||
|
|
@ -82,21 +76,18 @@ export function SupersetDashboard({
|
|||
},
|
||||
});
|
||||
|
||||
// O SDK cria um <iframe> sem width/height explícitos — forçar 100%
|
||||
const iframe = containerRef.current?.querySelector("iframe");
|
||||
if (iframe) {
|
||||
iframe.style.width = "100%";
|
||||
iframe.style.height = "100%";
|
||||
iframe.style.border = "none";
|
||||
}
|
||||
|
||||
setStatus("ready");
|
||||
} catch (err: any) {
|
||||
console.error("[SupersetDashboard] Erro:", err.message, err);
|
||||
setErrorMsg(err.message || "Erro desconhecido");
|
||||
setStatus("error");
|
||||
console.error("[SupersetDashboard] Erro:", err.message);
|
||||
// Se SDK não estiver instalado, usa iframe como fallback
|
||||
if (err.message?.includes("Cannot find module") || err.message?.includes("embedDashboard")) {
|
||||
setStatus("unavailable");
|
||||
} else {
|
||||
setErrorMsg(err.message);
|
||||
setStatus("error");
|
||||
}
|
||||
}
|
||||
}, [dashboardId, fetchTokenData]);
|
||||
}, [dashboardId, fetchGuestToken]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboard();
|
||||
|
|
@ -109,16 +100,13 @@ export function SupersetDashboard({
|
|||
|
||||
if (status === "unavailable") {
|
||||
return (
|
||||
<div className={`flex flex-col items-center justify-center gap-4 rounded-xl border border-yellow-100 bg-yellow-50 ${className}`} style={{ height: heightStyle }}>
|
||||
<AlertTriangle className="w-8 h-8 text-yellow-500" />
|
||||
<div className="text-center">
|
||||
<p className="font-medium text-yellow-800 text-sm">Arcádia Insights indisponível</p>
|
||||
<p className="text-yellow-600 text-xs mt-1">O serviço de BI está iniciando. Tente novamente em alguns segundos.</p>
|
||||
</div>
|
||||
<button onClick={loadDashboard} className="flex items-center gap-2 px-4 py-2 bg-yellow-600 text-white rounded-lg text-sm hover:bg-yellow-700 transition-colors">
|
||||
<RefreshCw className="w-4 h-4" /> Tentar novamente
|
||||
</button>
|
||||
<a href="https://bi.onboardbi.com.br" target="_blank" rel="noopener noreferrer" className="text-xs text-yellow-700 underline">Abrir em nova aba</a>
|
||||
<div className={`rounded-xl overflow-hidden border border-[#c89b3c]/20 bg-white shadow-sm ${className}`} style={{ height: heightStyle }}>
|
||||
<iframe
|
||||
src="/superset/login"
|
||||
className="w-full h-full border-0"
|
||||
title="Arcádia Insights — Apache Superset"
|
||||
allow="fullscreen"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -154,7 +142,7 @@ export function SupersetDashboard({
|
|||
|
||||
{showOpenLink && status === "ready" && (
|
||||
<a
|
||||
href="https://bi.onboardbi.com.br"
|
||||
href="/superset"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="absolute top-3 right-3 z-10 flex items-center gap-1 px-2 py-1 bg-white/80 backdrop-blur text-[#1f334d] text-xs rounded-lg border border-gray-200 hover:bg-white transition-colors shadow-sm"
|
||||
|
|
@ -163,16 +151,9 @@ export function SupersetDashboard({
|
|||
</a>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.superset-embed-container iframe {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border: none !important;
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="superset-embed-container w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20"
|
||||
className="w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export default function TradeInForm({ onClose, onSave, initialEvaluation, custom
|
|||
useEffect(() => {
|
||||
const loadPersons = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/soe/persons", { credentials: "include" });
|
||||
const res = await fetch("/api/erp/persons", { credentials: "include" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setPersonsList(data);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* OpenClawWidget.tsx
|
||||
*
|
||||
* Widget flutuante do OpenClaw — aparece quando há sugestões pendentes.
|
||||
* Posicionado no canto inferior direito, fora das rotas.
|
||||
* Abre o modal SkillSuggestion para confirmação.
|
||||
*
|
||||
* Fase 4: OpenClaw Sprint 2
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Sparkles, X, ChevronUp, ChevronDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useAgentEmergence } from "@/hooks/useAgentEmergence";
|
||||
import { SkillSuggestion } from "./SkillSuggestion";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
|
||||
export function OpenClawWidget() {
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
suggestions,
|
||||
activeSuggestion,
|
||||
pendingCount,
|
||||
confirmSkill,
|
||||
rejectSkill,
|
||||
openSuggestion,
|
||||
closeSuggestion,
|
||||
} = useAgentEmergence();
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
// Não renderiza se não há usuário, não há sugestões, ou foi dispensado
|
||||
if (!user || pendingCount === 0 || dismissed) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Widget flutuante */}
|
||||
<div
|
||||
className="fixed bottom-6 right-6 z-50 flex flex-col items-end gap-2"
|
||||
role="complementary"
|
||||
aria-label="OpenClaw - Sugestões de Skills"
|
||||
>
|
||||
{/* Painel expandido */}
|
||||
{!collapsed && (
|
||||
<div className="w-72 rounded-xl border bg-background shadow-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-2.5 border-b bg-violet-50 dark:bg-violet-900/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-violet-600 dark:text-violet-400" />
|
||||
<span className="text-sm font-semibold text-violet-700 dark:text-violet-300">
|
||||
OpenClaw
|
||||
</span>
|
||||
<Badge className="bg-violet-600 text-white text-xs px-1.5 py-0">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setCollapsed(true)}
|
||||
className="p-0.5 rounded hover:bg-violet-100 dark:hover:bg-violet-900/40 text-muted-foreground"
|
||||
aria-label="Minimizar"
|
||||
>
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDismissed(true)}
|
||||
className="p-0.5 rounded hover:bg-violet-100 dark:hover:bg-violet-900/40 text-muted-foreground"
|
||||
aria-label="Fechar"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-2 max-h-64 overflow-y-auto">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Detectei padrões de uso que podem virar skills automáticas:
|
||||
</p>
|
||||
{suggestions.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => openSuggestion(s)}
|
||||
className="w-full text-left rounded-lg border p-2.5 hover:bg-muted/50 transition-colors space-y-1"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="text-sm font-medium leading-tight line-clamp-1">
|
||||
{s.suggested_skill_name}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs shrink-0">
|
||||
{Math.round(s.confidence * 100)}%
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||
{s.suggested_description}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Botão minimizado */}
|
||||
{collapsed && (
|
||||
<button
|
||||
onClick={() => setCollapsed(false)}
|
||||
className="flex items-center gap-2 rounded-full bg-violet-600 hover:bg-violet-700 text-white px-4 py-2.5 shadow-lg transition-colors"
|
||||
aria-label="Expandir sugestões de skills"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Skills sugeridas</span>
|
||||
<Badge className="bg-white text-violet-700 text-xs px-1.5 py-0 ml-0.5">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
<ChevronUp className="h-3.5 w-3.5 ml-0.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal de detalhes */}
|
||||
{activeSuggestion && (
|
||||
<SkillSuggestion
|
||||
suggestion={activeSuggestion}
|
||||
onConfirm={confirmSkill}
|
||||
onReject={rejectSkill}
|
||||
onClose={closeSuggestion}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* SkillSuggestion.tsx
|
||||
*
|
||||
* Modal com preview da skill sugerida pelo OpenClaw.
|
||||
* Apresenta padrão detectado, nome, descrição e preview de automação.
|
||||
* Usuário pode confirmar ou rejeitar.
|
||||
*
|
||||
* Fase 4: OpenClaw Sprint 2
|
||||
*/
|
||||
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Sparkles, X, Check, Zap, BarChart2 } from "lucide-react";
|
||||
import type { SkillSuggestionData } from "@/hooks/useAgentEmergence";
|
||||
|
||||
interface SkillSuggestionProps {
|
||||
suggestion: SkillSuggestionData;
|
||||
onConfirm: (id: string) => void;
|
||||
onReject: (id: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SkillSuggestion({ suggestion, onConfirm, onReject, onClose }: SkillSuggestionProps) {
|
||||
const confidencePct = Math.round(suggestion.confidence * 100);
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-md bg-violet-100 dark:bg-violet-900/30">
|
||||
<Sparkles className="h-4 w-4 text-violet-600 dark:text-violet-400" />
|
||||
</div>
|
||||
<DialogTitle className="text-base">Nova skill detectada</DialogTitle>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Padrão detectado */}
|
||||
<div className="rounded-lg border bg-muted/40 p-3 text-sm space-y-1">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Padrão detectado</p>
|
||||
<p className="font-medium">{suggestion.pattern.description || suggestion.pattern.action_type}</p>
|
||||
<div className="flex items-center gap-3 text-muted-foreground text-xs mt-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<BarChart2 className="h-3 w-3" />
|
||||
{suggestion.pattern.frequency}x detectado
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Zap className="h-3 w-3" />
|
||||
Confiança: {confidencePct}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skill sugerida */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-1">Nome da skill</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
{suggestion.suggested_skill_name}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-1">Descrição</p>
|
||||
<p className="text-sm">{suggestion.suggested_description}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-1">O que será automatizado</p>
|
||||
<div className="rounded-md border bg-muted/20 p-2.5 text-sm text-muted-foreground">
|
||||
{suggestion.estimated_automation}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onReject(suggestion.id)}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
Rejeitar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onConfirm(suggestion.id)}
|
||||
className="gap-1.5 bg-violet-600 hover:bg-violet-700"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
Criar skill
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,63 +1,25 @@
|
|||
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
|
||||
import { createContext, useContext, useState, ReactNode } from "react";
|
||||
|
||||
export type SoeMotor = "plus" | "erpnext";
|
||||
|
||||
interface SoeMotorContextType {
|
||||
motor: SoeMotor;
|
||||
setMotor: (motor: SoeMotor) => void;
|
||||
usePlus: boolean;
|
||||
useERPNext: boolean;
|
||||
getApiUrl: (localPath: string, plusPath: string, erpnextPath?: string) => string;
|
||||
profile: SoeMotor;
|
||||
setProfile: (motor: SoeMotor) => void;
|
||||
interface SoeMotorContextValue {
|
||||
activeCompany: string | null;
|
||||
setActiveCompany: (id: string | null) => void;
|
||||
}
|
||||
|
||||
const SoeMotorCtx = createContext<SoeMotorContextType | null>(null);
|
||||
const SoeMotorContext = createContext<SoeMotorContextValue>({
|
||||
activeCompany: null,
|
||||
setActiveCompany: () => {},
|
||||
});
|
||||
|
||||
export function SoeMotorProvider({ children }: { children: ReactNode }) {
|
||||
const [motor, setMotorState] = useState<SoeMotor>(() => {
|
||||
const saved = localStorage.getItem("arcadia_soe_motor") || localStorage.getItem("arcadia_erp_profile");
|
||||
return (saved as SoeMotor) || "plus";
|
||||
});
|
||||
|
||||
const setMotor = (newMotor: SoeMotor) => {
|
||||
setMotorState(newMotor);
|
||||
localStorage.setItem("arcadia_soe_motor", newMotor);
|
||||
localStorage.setItem("arcadia_erp_profile", newMotor);
|
||||
};
|
||||
|
||||
const usePlus = motor === "plus";
|
||||
const useERPNext = motor === "erpnext";
|
||||
|
||||
const getApiUrl = (localPath: string, plusPath: string, erpnextPath?: string): string => {
|
||||
if (usePlus) {
|
||||
return `/plus/api${plusPath}`;
|
||||
}
|
||||
if (useERPNext && erpnextPath) {
|
||||
return erpnextPath;
|
||||
}
|
||||
return localPath;
|
||||
};
|
||||
const [activeCompany, setActiveCompany] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<SoeMotorCtx.Provider value={{ motor, setMotor, usePlus, useERPNext, getApiUrl, profile: motor, setProfile: setMotor }}>
|
||||
<SoeMotorContext.Provider value={{ activeCompany, setActiveCompany }}>
|
||||
{children}
|
||||
</SoeMotorCtx.Provider>
|
||||
</SoeMotorContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSoeMotor() {
|
||||
const context = useContext(SoeMotorCtx);
|
||||
if (!context) {
|
||||
throw new Error("useSoeMotor must be used within SoeMotorProvider");
|
||||
}
|
||||
return context;
|
||||
return useContext(SoeMotorContext);
|
||||
}
|
||||
|
||||
export function useErpProfile() {
|
||||
const ctx = useSoeMotor();
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export type ErpProfile = SoeMotor;
|
||||
export const ErpProfileProvider = SoeMotorProvider;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* useAgentEmergence.ts
|
||||
*
|
||||
* Hook para gerenciar sugestões de skills do OpenClaw.
|
||||
* Escuta eventos Socket.IO e coordena confirmações/rejeições.
|
||||
*
|
||||
* Fase 4: OpenClaw Sprint 2
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useAuth } from "./use-auth";
|
||||
|
||||
export interface SkillSuggestionData {
|
||||
id: string;
|
||||
pattern: {
|
||||
id: string;
|
||||
action_type: string;
|
||||
frequency: number;
|
||||
confidence: number;
|
||||
description: string;
|
||||
};
|
||||
suggested_skill_name: string;
|
||||
suggested_description: string;
|
||||
estimated_automation: string;
|
||||
confidence: number;
|
||||
created_at: string;
|
||||
status: "pending" | "accepted" | "rejected";
|
||||
}
|
||||
|
||||
interface UseAgentEmergenceReturn {
|
||||
suggestions: SkillSuggestionData[];
|
||||
activeSuggestion: SkillSuggestionData | null;
|
||||
isOpen: boolean;
|
||||
pendingCount: number;
|
||||
confirmSkill: (suggestionId: string) => Promise<void>;
|
||||
rejectSkill: (suggestionId: string) => Promise<void>;
|
||||
openSuggestion: (suggestion: SkillSuggestionData) => void;
|
||||
closeSuggestion: () => void;
|
||||
dismissWidget: () => void;
|
||||
}
|
||||
|
||||
export function useAgentEmergence(): UseAgentEmergenceReturn {
|
||||
const { user } = useAuth();
|
||||
const [suggestions, setSuggestions] = useState<SkillSuggestionData[]>([]);
|
||||
const [activeSuggestion, setActiveSuggestion] = useState<SkillSuggestionData | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Carrega sugestões pendentes do servidor
|
||||
const fetchPendingSuggestions = useCallback(async () => {
|
||||
if (!user) return;
|
||||
try {
|
||||
const res = await fetch("/api/openclaw/suggestions");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSuggestions(data.suggestions || []);
|
||||
}
|
||||
} catch {
|
||||
// silencioso — não bloqueia o usuário
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// Escuta eventos Socket.IO via window (padrão da suite)
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
|
||||
fetchPendingSuggestions();
|
||||
|
||||
const handleOpenClawEvent = (event: CustomEvent) => {
|
||||
const suggestion: SkillSuggestionData = event.detail;
|
||||
setSuggestions((prev) => {
|
||||
const exists = prev.find((s) => s.id === suggestion.id);
|
||||
if (exists) return prev;
|
||||
return [suggestion, ...prev];
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("openclaw:suggestion" as any, handleOpenClawEvent);
|
||||
return () => {
|
||||
window.removeEventListener("openclaw:suggestion" as any, handleOpenClawEvent);
|
||||
};
|
||||
}, [user, fetchPendingSuggestions]);
|
||||
|
||||
const confirmSkill = useCallback(async (suggestionId: string) => {
|
||||
try {
|
||||
const res = await fetch("/api/openclaw/confirm-skill", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ suggestion_id: suggestionId, action: "confirm" }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSuggestions((prev) => prev.filter((s) => s.id !== suggestionId));
|
||||
setActiveSuggestion(null);
|
||||
setIsOpen(false);
|
||||
}
|
||||
} catch {
|
||||
// erro tratado na UI via toast externo
|
||||
}
|
||||
}, []);
|
||||
|
||||
const rejectSkill = useCallback(async (suggestionId: string) => {
|
||||
try {
|
||||
const res = await fetch("/api/openclaw/confirm-skill", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ suggestion_id: suggestionId, action: "reject" }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSuggestions((prev) => prev.filter((s) => s.id !== suggestionId));
|
||||
setActiveSuggestion(null);
|
||||
setIsOpen(false);
|
||||
}
|
||||
} catch {
|
||||
// silencioso
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openSuggestion = useCallback((suggestion: SkillSuggestionData) => {
|
||||
setActiveSuggestion(suggestion);
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeSuggestion = useCallback(() => {
|
||||
setActiveSuggestion(null);
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const dismissWidget = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
setActiveSuggestion(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
activeSuggestion,
|
||||
isOpen,
|
||||
pendingCount: suggestions.length,
|
||||
confirmSkill,
|
||||
rejectSkill,
|
||||
openSuggestion,
|
||||
closeSuggestion,
|
||||
dismissWidget,
|
||||
};
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ export function ProtectedRoute({
|
|||
component: Component,
|
||||
}: {
|
||||
path: string;
|
||||
component: () => React.JSX.Element;
|
||||
component: React.ComponentType<any>;
|
||||
}) {
|
||||
const { user, isLoading } = useAuth();
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -361,18 +361,6 @@ async function fetchManusRun(id: number): Promise<ManusRun> {
|
|||
return response.json();
|
||||
}
|
||||
|
||||
async function cancelManusRun(id: number): Promise<void> {
|
||||
await fetch(`/api/manus/runs/${id}`, { method: "DELETE", credentials: "include" });
|
||||
}
|
||||
|
||||
async function deleteManusRun(id: number): Promise<void> {
|
||||
await fetch(`/api/manus/runs/${id}`, { method: "DELETE", credentials: "include" });
|
||||
}
|
||||
|
||||
async function deleteAllManusRuns(): Promise<void> {
|
||||
await fetch("/api/manus/runs", { method: "DELETE", credentials: "include" });
|
||||
}
|
||||
|
||||
async function startManusRun(data: { prompt: string; attachedFiles?: AttachedFile[]; conversationHistory?: Array<{role: string; content: string}> }): Promise<{ runId: number }> {
|
||||
const response = await fetch("/api/manus/run", {
|
||||
method: "POST",
|
||||
|
|
@ -1254,13 +1242,7 @@ export default function Agent() {
|
|||
if (line.startsWith("data: ")) {
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6));
|
||||
if (data.tool_status) {
|
||||
setCurrentToolName(data.tool_status);
|
||||
setProcessingMode("using_tools");
|
||||
}
|
||||
if (data.content) {
|
||||
setCurrentToolName(null);
|
||||
setProcessingMode("idle");
|
||||
fullContent += data.content;
|
||||
setStreamingContent(fullContent);
|
||||
}
|
||||
|
|
@ -1499,21 +1481,8 @@ export default function Agent() {
|
|||
Nova Tarefa
|
||||
</Button>
|
||||
</div>
|
||||
<div className="px-3 pt-3 pb-1 flex items-center justify-between">
|
||||
<div className="px-3 pt-3 pb-1">
|
||||
<p className="text-xs text-white/40 uppercase tracking-wider font-medium">Todas as Tarefas</p>
|
||||
{manusRuns.length > 0 && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await deleteAllManusRuns();
|
||||
setSelectedRun(null);
|
||||
queryClient.invalidateQueries({ queryKey: ["manus-runs"] });
|
||||
}}
|
||||
className="text-[10px] text-white/30 hover:text-red-400 transition-colors"
|
||||
title="Limpar histórico"
|
||||
>
|
||||
Limpar tudo
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ScrollArea className="flex-1 px-2">
|
||||
{loadingRuns ? (
|
||||
|
|
@ -1525,35 +1494,22 @@ export default function Agent() {
|
|||
) : (
|
||||
<div className="space-y-1 pb-2">
|
||||
{manusRuns.map((run) => (
|
||||
<div key={run.id} className="group relative">
|
||||
<button
|
||||
onClick={() => setSelectedRun(run.id)}
|
||||
className={`w-full text-left p-2.5 rounded-lg transition-all pr-8 ${
|
||||
selectedRun === run.id
|
||||
? "bg-[#c89b3c]/20 border border-[#c89b3c]/50"
|
||||
: "hover:bg-[#162638] border border-transparent"
|
||||
}`}
|
||||
data-testid={`run-${run.id}`}
|
||||
>
|
||||
<p className="text-xs text-white line-clamp-2 mb-1.5">{run.prompt}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusBadge(run.status)}
|
||||
<span className="text-[10px] text-white/40">{formatTime(run.createdAt)}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await deleteManusRun(run.id);
|
||||
if (selectedRun === run.id) setSelectedRun(null);
|
||||
queryClient.invalidateQueries({ queryKey: ["manus-runs"] });
|
||||
}}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 text-white/30 hover:text-red-400 transition-all"
|
||||
title="Excluir"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
key={run.id}
|
||||
onClick={() => setSelectedRun(run.id)}
|
||||
className={`w-full text-left p-2.5 rounded-lg transition-all ${
|
||||
selectedRun === run.id
|
||||
? "bg-[#c89b3c]/20 border border-[#c89b3c]/50"
|
||||
: "hover:bg-[#162638] border border-transparent"
|
||||
}`}
|
||||
data-testid={`run-${run.id}`}
|
||||
>
|
||||
<p className="text-xs text-white line-clamp-2 mb-1.5">{run.prompt}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusBadge(run.status)}
|
||||
<span className="text-[10px] text-white/40">{formatTime(run.createdAt)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1973,21 +1929,6 @@ export default function Agent() {
|
|||
className="flex-1 border-[#e1e8f0] focus:border-[#c89b3c] focus:ring-[#c89b3c]/20"
|
||||
data-testid="input-message"
|
||||
/>
|
||||
{isStreaming && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (selectedRun) await cancelManusRun(selectedRun);
|
||||
setIsStreaming(false);
|
||||
setProcessingMode("idle");
|
||||
}}
|
||||
variant="destructive"
|
||||
className="shrink-0"
|
||||
data-testid="button-cancel-run"
|
||||
title="Cancelar"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!chatInput.trim() || isStreaming}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Lock,
|
||||
Search,
|
||||
ShoppingCart,
|
||||
GraduationCap,
|
||||
|
|
@ -53,6 +55,7 @@ interface AppItem {
|
|||
status: "active" | "coming_soon" | "beta";
|
||||
featured?: boolean;
|
||||
color: string;
|
||||
subscriptionCode?: string; // marketplace module code — if set, requires active subscription
|
||||
}
|
||||
|
||||
const apps: AppItem[] = [
|
||||
|
|
@ -80,11 +83,11 @@ const apps: AppItem[] = [
|
|||
color: "from-orange-500 to-red-500",
|
||||
},
|
||||
{
|
||||
id: "soe",
|
||||
name: "Arcádia SOE",
|
||||
description: "Sistema Operacional Empresarial - Kernel de negócios",
|
||||
id: "erp",
|
||||
name: "Arcádia ERP",
|
||||
description: "Gestão empresarial completa integrada ao ERPNext",
|
||||
icon: <Building className="w-8 h-8" />,
|
||||
route: "/soe",
|
||||
route: "/erp",
|
||||
category: "negocios",
|
||||
status: "active",
|
||||
featured: true,
|
||||
|
|
@ -500,6 +503,24 @@ export default function AppCenter() {
|
|||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [activeCategory, setActiveCategory] = useState("todos");
|
||||
|
||||
const { data: myAppsData } = useQuery<{ subscribedCodes: string[] }>({
|
||||
queryKey: ["/api/marketplace/my-apps"],
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const subscribedCodes = new Set(myAppsData?.subscribedCodes || []);
|
||||
|
||||
const isLocked = (app: AppItem) =>
|
||||
!!app.subscriptionCode && subscribedCodes.size > 0 && !subscribedCodes.has(app.subscriptionCode);
|
||||
|
||||
const handleAppClick = (app: AppItem) => {
|
||||
if (isLocked(app)) {
|
||||
setLocation("/marketplace");
|
||||
} else {
|
||||
setLocation(app.route);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredApps = apps.filter(app => {
|
||||
const matchesSearch = app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
app.description.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
|
@ -546,7 +567,7 @@ export default function AppCenter() {
|
|||
<Card
|
||||
key={app.id}
|
||||
className="bg-white/5 border-white/10 hover:border-white/30 cursor-pointer transition-all hover:scale-105 group"
|
||||
onClick={() => setLocation(app.route)}
|
||||
onClick={() => handleAppClick(app)}
|
||||
data-testid={`featured-app-${app.id}`}
|
||||
>
|
||||
<CardContent className="p-4 text-center">
|
||||
|
|
@ -579,27 +600,37 @@ export default function AppCenter() {
|
|||
|
||||
<TabsContent value={activeCategory} className="mt-0">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{filteredApps.map(app => (
|
||||
{filteredApps.map(app => {
|
||||
const locked = isLocked(app);
|
||||
return (
|
||||
<Card
|
||||
key={app.id}
|
||||
className="bg-white/5 border-white/10 hover:border-white/30 cursor-pointer transition-all group overflow-hidden"
|
||||
onClick={() => setLocation(app.route)}
|
||||
className={`border-white/10 cursor-pointer transition-all group overflow-hidden ${locked ? "bg-white/2 opacity-60 hover:opacity-80" : "bg-white/5 hover:border-white/30"}`}
|
||||
onClick={() => handleAppClick(app)}
|
||||
data-testid={`app-card-${app.id}`}
|
||||
>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${app.color} flex items-center justify-center text-white flex-shrink-0 group-hover:scale-110 transition-transform`}>
|
||||
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${app.color} flex items-center justify-center text-white flex-shrink-0 ${locked ? "" : "group-hover:scale-110"} transition-transform relative`}>
|
||||
{app.icon}
|
||||
{locked && (
|
||||
<div className="absolute inset-0 rounded-2xl bg-black/50 flex items-center justify-center">
|
||||
<Lock className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-white group-hover:text-indigo-400 transition-colors truncate">
|
||||
<h3 className={`font-semibold truncate ${locked ? "text-slate-400" : "text-white group-hover:text-indigo-400"} transition-colors`}>
|
||||
{app.name}
|
||||
</h3>
|
||||
{app.status === "beta" && (
|
||||
{locked && (
|
||||
<Badge className="bg-slate-600/50 text-slate-400 text-xs">Não contratado</Badge>
|
||||
)}
|
||||
{!locked && app.status === "beta" && (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-300 text-xs">Beta</Badge>
|
||||
)}
|
||||
{app.status === "coming_soon" && (
|
||||
{!locked && app.status === "coming_soon" && (
|
||||
<Badge className="bg-slate-500/20 text-slate-300 text-xs">Em breve</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -607,13 +638,20 @@ export default function AppCenter() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<span className="text-xs text-indigo-400 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Abrir <ArrowRight className="w-3 h-3" />
|
||||
</span>
|
||||
{locked ? (
|
||||
<span className="text-xs text-slate-500 flex items-center gap-1">
|
||||
Ver no Marketplace <ArrowRight className="w-3 h-3" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-indigo-400 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Abrir <ArrowRight className="w-3 h-3" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredApps.length === 0 && (
|
||||
|
|
|
|||
|
|
@ -150,18 +150,18 @@ const api = {
|
|||
|
||||
function DashboardModule({ tenantId }: { tenantId: number }) {
|
||||
const { data: stats = { customers: 0, suppliers: 0, products: 0, salesOrders: 0, purchaseOrders: 0 } } = useQuery({
|
||||
queryKey: ["/api/soe/stats", tenantId],
|
||||
queryFn: () => api.get(`/api/soe/stats?tenantId=${tenantId}`)
|
||||
queryKey: ["/api/erp/stats", tenantId],
|
||||
queryFn: () => api.get(`/api/erp/stats?tenantId=${tenantId}`)
|
||||
});
|
||||
|
||||
const { data: customers = [] } = useQuery<Customer[]>({
|
||||
queryKey: ["/api/soe/customers", tenantId],
|
||||
queryFn: () => api.get(`/api/soe/customers?tenantId=${tenantId}`)
|
||||
queryKey: ["/api/erp/customers", tenantId],
|
||||
queryFn: () => api.get(`/api/erp/customers?tenantId=${tenantId}`)
|
||||
});
|
||||
|
||||
const { data: salesOrders = [] } = useQuery<SalesOrder[]>({
|
||||
queryKey: ["/api/soe/sales-orders", tenantId],
|
||||
queryFn: () => api.get(`/api/soe/sales-orders?tenantId=${tenantId}`)
|
||||
queryKey: ["/api/erp/sales-orders", tenantId],
|
||||
queryFn: () => api.get(`/api/erp/sales-orders?tenantId=${tenantId}`)
|
||||
});
|
||||
|
||||
const totalRevenue = salesOrders
|
||||
|
|
@ -296,14 +296,14 @@ function PersonsModule({ tenantId }: { tenantId: number }) {
|
|||
const [formStatus, setFormStatus] = useState("active");
|
||||
|
||||
const { data: persons = [], isLoading, refetch } = useQuery<Person[]>({
|
||||
queryKey: ["/api/soe/persons", tenantId, roleFilter],
|
||||
queryFn: () => api.get(`/api/soe/persons?tenantId=${tenantId}&role=${roleFilter}`)
|
||||
queryKey: ["/api/erp/persons", tenantId, roleFilter],
|
||||
queryFn: () => api.get(`/api/erp/persons?tenantId=${tenantId}&role=${roleFilter}`)
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post("/api/soe/persons", { ...data, tenantId }),
|
||||
mutationFn: (data: any) => api.post("/api/erp/persons", { ...data, tenantId }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/persons"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/persons"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
toast({ title: "Pessoa criada com sucesso!" });
|
||||
|
|
@ -312,9 +312,9 @@ function PersonsModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/persons/${id}`, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/persons/${id}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/persons"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/persons"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
toast({ title: "Pessoa atualizada!" });
|
||||
|
|
@ -322,9 +322,9 @@ function PersonsModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => api.delete(`/api/soe/persons/${id}`),
|
||||
mutationFn: (id: number) => api.delete(`/api/erp/persons/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/persons"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/persons"] });
|
||||
toast({ title: "Pessoa excluída!" });
|
||||
}
|
||||
});
|
||||
|
|
@ -601,15 +601,15 @@ function CustomersModule({ tenantId }: { tenantId: number }) {
|
|||
const [formStatus, setFormStatus] = useState("active");
|
||||
|
||||
const { data: customers = [], isLoading, refetch } = useQuery<Customer[]>({
|
||||
queryKey: ["/api/soe/customers", tenantId],
|
||||
queryFn: () => api.get(`/api/soe/customers?tenantId=${tenantId}`)
|
||||
queryKey: ["/api/erp/customers", tenantId],
|
||||
queryFn: () => api.get(`/api/erp/customers?tenantId=${tenantId}`)
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post("/api/soe/customers", { ...data, tenantId }),
|
||||
mutationFn: (data: any) => api.post("/api/erp/customers", { ...data, tenantId }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/customers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/customers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
toast({ title: "Cliente criado com sucesso!" });
|
||||
|
|
@ -618,9 +618,9 @@ function CustomersModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/customers/${id}`, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/customers/${id}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/customers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/customers"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
toast({ title: "Cliente atualizado!" });
|
||||
|
|
@ -628,10 +628,10 @@ function CustomersModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => api.delete(`/api/soe/customers/${id}`),
|
||||
mutationFn: (id: number) => api.delete(`/api/erp/customers/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/customers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/customers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
|
||||
toast({ title: "Cliente excluído!" });
|
||||
}
|
||||
});
|
||||
|
|
@ -837,15 +837,15 @@ function SuppliersModule({ tenantId }: { tenantId: number }) {
|
|||
const [formStatus, setFormStatus] = useState("active");
|
||||
|
||||
const { data: suppliers = [], isLoading, refetch } = useQuery<Supplier[]>({
|
||||
queryKey: ["/api/soe/suppliers", tenantId],
|
||||
queryFn: () => api.get(`/api/soe/suppliers?tenantId=${tenantId}`)
|
||||
queryKey: ["/api/erp/suppliers", tenantId],
|
||||
queryFn: () => api.get(`/api/erp/suppliers?tenantId=${tenantId}`)
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post("/api/soe/suppliers", { ...data, tenantId }),
|
||||
mutationFn: (data: any) => api.post("/api/erp/suppliers", { ...data, tenantId }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/suppliers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/suppliers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
toast({ title: "Fornecedor criado com sucesso!" });
|
||||
|
|
@ -853,9 +853,9 @@ function SuppliersModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/suppliers/${id}`, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/suppliers/${id}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/suppliers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/suppliers"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
toast({ title: "Fornecedor atualizado!" });
|
||||
|
|
@ -863,10 +863,10 @@ function SuppliersModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => api.delete(`/api/soe/suppliers/${id}`),
|
||||
mutationFn: (id: number) => api.delete(`/api/erp/suppliers/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/suppliers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/suppliers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
|
||||
toast({ title: "Fornecedor excluído!" });
|
||||
}
|
||||
});
|
||||
|
|
@ -1052,8 +1052,8 @@ function ProductsModule({ tenantId }: { tenantId: number }) {
|
|||
const [formTaxGroup, setFormTaxGroup] = useState("");
|
||||
|
||||
const { data: products = [], isLoading, refetch } = useQuery<Product[]>({
|
||||
queryKey: ["/api/soe/products", tenantId],
|
||||
queryFn: () => api.get(`/api/soe/products?tenantId=${tenantId}`)
|
||||
queryKey: ["/api/erp/products", tenantId],
|
||||
queryFn: () => api.get(`/api/erp/products?tenantId=${tenantId}`)
|
||||
});
|
||||
|
||||
const { data: taxGroups = [] } = useQuery<TaxGroup[]>({
|
||||
|
|
@ -1062,10 +1062,10 @@ function ProductsModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post("/api/soe/products", { ...data, tenantId }),
|
||||
mutationFn: (data: any) => api.post("/api/erp/products", { ...data, tenantId }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/products"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/products"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
toast({ title: "Produto criado com sucesso!" });
|
||||
|
|
@ -1073,9 +1073,9 @@ function ProductsModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/products/${id}`, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/products/${id}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/products"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/products"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
toast({ title: "Produto atualizado!" });
|
||||
|
|
@ -1083,10 +1083,10 @@ function ProductsModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => api.delete(`/api/soe/products/${id}`),
|
||||
mutationFn: (id: number) => api.delete(`/api/erp/products/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/products"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/products"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
|
||||
toast({ title: "Produto excluído!" });
|
||||
}
|
||||
});
|
||||
|
|
@ -1316,20 +1316,20 @@ function SalesModule({ tenantId }: { tenantId: number }) {
|
|||
const [formCustomerId, setFormCustomerId] = useState("");
|
||||
|
||||
const { data: salesOrders = [], isLoading, refetch } = useQuery<SalesOrder[]>({
|
||||
queryKey: ["/api/soe/sales-orders", tenantId],
|
||||
queryFn: () => api.get(`/api/soe/sales-orders?tenantId=${tenantId}`)
|
||||
queryKey: ["/api/erp/sales-orders", tenantId],
|
||||
queryFn: () => api.get(`/api/erp/sales-orders?tenantId=${tenantId}`)
|
||||
});
|
||||
|
||||
const { data: customers = [] } = useQuery<Customer[]>({
|
||||
queryKey: ["/api/soe/customers", tenantId],
|
||||
queryFn: () => api.get(`/api/soe/customers?tenantId=${tenantId}`)
|
||||
queryKey: ["/api/erp/customers", tenantId],
|
||||
queryFn: () => api.get(`/api/erp/customers?tenantId=${tenantId}`)
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post("/api/soe/sales-orders", { ...data, tenantId }),
|
||||
mutationFn: (data: any) => api.post("/api/erp/sales-orders", { ...data, tenantId }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/sales-orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/sales-orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
setFormCustomerId("");
|
||||
|
|
@ -1338,10 +1338,10 @@ function SalesModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/soe/sales-orders/${id}`, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/api/erp/sales-orders/${id}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/sales-orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/sales-orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
|
||||
setShowDialog(false);
|
||||
setEditingItem(null);
|
||||
setFormCustomerId("");
|
||||
|
|
@ -1350,18 +1350,18 @@ function SalesModule({ tenantId }: { tenantId: number }) {
|
|||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => api.delete(`/api/soe/sales-orders/${id}`),
|
||||
mutationFn: (id: number) => api.delete(`/api/erp/sales-orders/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/sales-orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/sales-orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/stats"] });
|
||||
toast({ title: "Pedido excluído!" });
|
||||
}
|
||||
});
|
||||
|
||||
const confirmMutation = useMutation({
|
||||
mutationFn: (id: number) => api.post(`/api/soe/sales-orders/${id}/confirm`, {}),
|
||||
mutationFn: (id: number) => api.post(`/api/erp/sales-orders/${id}/confirm`, {}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/soe/sales-orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/erp/sales-orders"] });
|
||||
toast({ title: "Pedido confirmado!" });
|
||||
}
|
||||
});
|
||||
|
|
@ -1728,7 +1728,7 @@ export default function ArcadiaNext() {
|
|||
<Building2 className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-bold text-lg">Arcádia SOE</h1>
|
||||
<h1 className="font-bold text-lg">Arcádia ERP</h1>
|
||||
<p className="text-xs text-muted-foreground">Gestão Empresarial</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ function AutomationCard({ automation, onRefresh }: { automation: Automation; onR
|
|||
size="icon"
|
||||
onClick={() => runMutation.mutate()}
|
||||
disabled={runMutation.isPending}
|
||||
className="h-8 w-8 flex items-center justify-center text-white/50 hover:text-[#c89b3c] hover:bg-[#c89b3c]/10"
|
||||
className="h-8 w-8 text-white/50 hover:text-[#c89b3c] hover:bg-[#c89b3c]/10"
|
||||
title="Executar agora"
|
||||
data-testid={`run-automation-${automation.id}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
|
@ -47,9 +47,9 @@ import {
|
|||
X,
|
||||
Sparkles,
|
||||
MessageSquare,
|
||||
Brain,
|
||||
} from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -81,8 +81,73 @@ import {
|
|||
ResponsiveContainer,
|
||||
Cell,
|
||||
} from "recharts";
|
||||
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
||||
import { MiroFlowControl } from "@/components/MiroFlowControl";
|
||||
|
||||
// ── Componente da aba Insights (Apache Superset) ──────────────────────────────
|
||||
function SupersetAdvancedTab() {
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<string>("");
|
||||
const [dashboards, setDashboards] = useState<Array<{ id: string; title: string }>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/superset/dashboards", { credentials: "include" })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (Array.isArray(data)) {
|
||||
setDashboards(data.map((d: any) => ({ id: String(d.id), title: d.dashboard_title || d.title })));
|
||||
if (data.length > 0) setSelectedDashboard(String(data[0].id));
|
||||
}
|
||||
})
|
||||
.catch(() => setDashboards([]));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[#1f334d]">Arcádia Insights — Apache Superset</h2>
|
||||
<p className="text-sm text-gray-500">SQL Lab avançado, 50+ tipos de gráfico, dashboards interativos</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{dashboards.length > 0 && (
|
||||
<select
|
||||
value={selectedDashboard}
|
||||
onChange={e => setSelectedDashboard(e.target.value)}
|
||||
className="text-sm border border-gray-200 rounded-lg px-3 py-2 bg-white text-[#1f334d] focus:outline-none focus:ring-2 focus:ring-[#c89b3c]"
|
||||
>
|
||||
{dashboards.map(d => (
|
||||
<option key={d.id} value={d.id}>{d.title}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<a
|
||||
href="/superset"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[#1f334d] text-white rounded-lg hover:bg-[#2a4466] transition-colors text-sm"
|
||||
>
|
||||
<ArrowRight className="w-4 h-4" /> Abrir completo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedDashboard ? (
|
||||
<SupersetDashboard
|
||||
dashboardId={selectedDashboard}
|
||||
height="calc(100vh - 320px)"
|
||||
showOpenLink={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-xl overflow-hidden border border-[#c89b3c]/20 bg-white shadow-sm" style={{ height: "calc(100vh - 320px)" }}>
|
||||
<iframe
|
||||
src="/superset"
|
||||
className="w-full h-full border-0"
|
||||
title="Arcádia Insights — Apache Superset"
|
||||
allow="fullscreen"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface BiStats {
|
||||
dataSources: number;
|
||||
|
|
@ -2895,9 +2960,6 @@ export default function BiWorkspace() {
|
|||
<TabsTrigger value="advanced" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
|
||||
<Settings className="w-4 h-4 mr-2" /> Insights
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cientifico" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
|
||||
<Brain className="w-4 h-4 mr-2" /> Científico
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="mt-0">
|
||||
|
|
@ -2922,27 +2984,8 @@ export default function BiWorkspace() {
|
|||
<StagingTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced" className="mt-0">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[#1f334d]">Arcádia Insights — Apache Superset</h2>
|
||||
<p className="text-sm text-gray-500">SQL Lab avançado, 50+ tipos de gráfico, dashboards interativos</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://bi.onboardbi.com.br"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[#1f334d] text-white rounded-lg hover:bg-[#2a4466] transition-colors text-sm"
|
||||
data-testid="link-open-superset-external"
|
||||
>
|
||||
<ArrowRight className="w-4 h-4" /> Abrir em Nova Aba
|
||||
</a>
|
||||
</div>
|
||||
<SupersetDashboard dashboardId="executive-summary" height="calc(100vh - 320px)" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="cientifico" className="mt-0">
|
||||
<MiroFlowControl />
|
||||
<SupersetAdvancedTab />
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import {
|
|||
Link,
|
||||
File
|
||||
} from "lucide-react";
|
||||
const browserIcon = "/arcadia_suite_icon.png";
|
||||
import browserIcon from "@assets/arcadia_branding/arcadia_suite_icon.png";
|
||||
import { DigitalClock } from "@/components/DigitalClock";
|
||||
|
||||
type ParaTab = "projetos" | "areas" | "recursos" | "arquivo";
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ export default function CommercialEnv() {
|
|||
Comercial - Engenharia Ambiental
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Propostas, clientes e pipeline de vendas integrado ao Arcádia SOE
|
||||
Propostas, clientes e pipeline de vendas integrado ao Arcádia ERP
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => refetchCustomers()} variant="outline" size="sm">
|
||||
|
|
@ -660,7 +660,7 @@ export default function CommercialEnv() {
|
|||
{salesOrders.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
|
||||
Nenhum pedido encontrado. Sincronize com o Arcádia SOE.
|
||||
Nenhum pedido encontrado. Sincronize com o Arcádia ERP.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
BookOpen,
|
||||
|
|
@ -392,13 +391,12 @@ export default function Contabil() {
|
|||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-6">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger>
|
||||
<TabsTrigger value="plano" data-testid="tab-plano">Plano de Contas</TabsTrigger>
|
||||
<TabsTrigger value="lancamentos" data-testid="tab-lancamentos">Lançamentos</TabsTrigger>
|
||||
<TabsTrigger value="centros" data-testid="tab-centros">Centros de Custo</TabsTrigger>
|
||||
<TabsTrigger value="sped" data-testid="tab-sped">SPED ECD</TabsTrigger>
|
||||
<TabsTrigger value="analise" data-testid="tab-analise">Análise BI</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="dashboard" className="space-y-4">
|
||||
|
|
@ -787,9 +785,6 @@ export default function Contabil() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="analise">
|
||||
<SupersetDashboard dashboardId="dre-mensal" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Dialog open={showContaDialog} onOpenChange={setShowContaDialog}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import MultiTenantSection from "@/components/MultiTenantSection";
|
||||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -43,8 +42,7 @@ import {
|
|||
Zap,
|
||||
AlertCircle,
|
||||
Clock,
|
||||
Briefcase,
|
||||
Building2
|
||||
Briefcase
|
||||
} from "lucide-react";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
|
@ -884,8 +882,8 @@ export default function Crm() {
|
|||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold" data-testid="text-crm-title">Manager Partners</h1>
|
||||
<p className="text-muted-foreground">Gestão de parceiros, tenants e contratos</p>
|
||||
<h1 className="text-3xl font-bold" data-testid="text-crm-title">Arcádia CRM</h1>
|
||||
<p className="text-muted-foreground">Gestão de parceiros, contratos e comunicação</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{googleStatus?.connected ? (
|
||||
|
|
@ -949,10 +947,6 @@ export default function Crm() {
|
|||
<Settings className="w-4 h-4 mr-2" />
|
||||
Configurações
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="multitenant" data-testid="tab-multitenant">
|
||||
<Building2 className="w-4 h-4 mr-2" />
|
||||
Multi-Tenant
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
|
|
@ -2328,7 +2322,7 @@ export default function Crm() {
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="erpnext">Frappe ERPNext</SelectItem>
|
||||
<SelectItem value="erpnext">Frappe Arcádia ERP</SelectItem>
|
||||
<SelectItem value="crm_next">Frappe Arcádia CRM</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
@ -2387,7 +2381,7 @@ export default function Crm() {
|
|||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Sistema:</span>
|
||||
<Badge variant="outline" className="capitalize">{connector.targetSystem === "erpnext" ? "ERPNext" : "Arcádia CRM"}</Badge>
|
||||
<Badge variant="outline" className="capitalize">{connector.targetSystem === "erpnext" ? "Arcádia ERP" : "Arcádia CRM"}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Status:</span>
|
||||
|
|
@ -2688,10 +2682,6 @@ export default function Crm() {
|
|||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="multitenant" className="space-y-4">
|
||||
<MultiTenantSection />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,26 +29,8 @@ import {
|
|||
HardDrive,
|
||||
Workflow,
|
||||
Users,
|
||||
Brain,
|
||||
Sparkles,
|
||||
Search,
|
||||
Code,
|
||||
Terminal,
|
||||
Globe,
|
||||
Shield,
|
||||
ArrowRight,
|
||||
Layers,
|
||||
Network,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface EngineStatus {
|
||||
name: string;
|
||||
|
|
@ -85,7 +67,6 @@ interface EngineRoomData {
|
|||
}
|
||||
|
||||
const ENGINE_ICONS: Record<string, any> = {
|
||||
"manus-ia": Brain,
|
||||
"plus": ShoppingCart,
|
||||
"contabil": Calculator,
|
||||
"fisco": FileText,
|
||||
|
|
@ -107,18 +88,13 @@ const STATUS_CONFIG = {
|
|||
error: { icon: AlertCircle, color: "text-amber-400", bg: "bg-amber-500/10", label: "Erro" },
|
||||
};
|
||||
|
||||
function EngineCard({ engine, onClick }: { engine: EngineStatus; onClick?: () => void }) {
|
||||
function EngineCard({ engine }: { engine: EngineStatus }) {
|
||||
const statusConf = STATUS_CONFIG[engine.status];
|
||||
const StatusIcon = statusConf.icon;
|
||||
const EngineIcon = ENGINE_ICONS[engine.name] || Server;
|
||||
const isClickable = !!onClick;
|
||||
|
||||
return (
|
||||
<Card
|
||||
data-testid={`engine-card-${engine.name}`}
|
||||
className={`bg-[#1a1a2e] border-[#2a2a4a] hover:border-[#3a3a5a] transition-all ${isClickable ? "cursor-pointer hover:shadow-lg hover:shadow-violet-500/5 hover:border-violet-500/30" : ""}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Card data-testid={`engine-card-${engine.name}`} className="bg-[#1a1a2e] border-[#2a2a4a] hover:border-[#3a3a5a] transition-all">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -130,16 +106,9 @@ function EngineCard({ engine, onClick }: { engine: EngineStatus; onClick?: () =>
|
|||
<p className="text-xs text-gray-400">{engine.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isClickable && (
|
||||
<Badge variant="outline" className="border-violet-500/30 text-violet-400 bg-violet-500/5 text-[10px]">
|
||||
Clique para detalhes
|
||||
</Badge>
|
||||
)}
|
||||
<div className={`flex items-center gap-1.5 px-2 py-1 rounded-full ${statusConf.bg}`}>
|
||||
<StatusIcon className={`w-3.5 h-3.5 ${statusConf.color}`} />
|
||||
<span className={`text-xs font-medium ${statusConf.color}`}>{statusConf.label}</span>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1.5 px-2 py-1 rounded-full ${statusConf.bg}`}>
|
||||
<StatusIcon className={`w-3.5 h-3.5 ${statusConf.color}`} />
|
||||
<span className={`text-xs font-medium ${statusConf.color}`}>{statusConf.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -191,208 +160,6 @@ function EngineCard({ engine, onClick }: { engine: EngineStatus; onClick?: () =>
|
|||
);
|
||||
}
|
||||
|
||||
function ManusStructurePanel({ open, onOpenChange, metrics }: { open: boolean; onOpenChange: (open: boolean) => void; metrics: any }) {
|
||||
const AGENTS = [
|
||||
{ name: "Architect", role: "Design & Arquitetura", icon: Layers, color: "text-blue-400", bg: "bg-blue-500/10" },
|
||||
{ name: "Generator", role: "Geração de Código", icon: Code, color: "text-green-400", bg: "bg-green-500/10" },
|
||||
{ name: "Validator", role: "Validação TypeScript", icon: Shield, color: "text-amber-400", bg: "bg-amber-500/10" },
|
||||
{ name: "Executor", role: "Execução & Staging", icon: Terminal, color: "text-red-400", bg: "bg-red-500/10" },
|
||||
{ name: "Researcher", role: "Pesquisa & Contexto", icon: Search, color: "text-cyan-400", bg: "bg-cyan-500/10" },
|
||||
{ name: "Evolution", role: "Aprendizado Evolutivo", icon: Sparkles, color: "text-purple-400", bg: "bg-purple-500/10" },
|
||||
];
|
||||
|
||||
const TOOL_CATEGORIES = [
|
||||
{ name: "Busca Semântica", count: 8, icon: Search, color: "text-violet-400" },
|
||||
{ name: "Leitura/Escrita de Arquivos", count: 12, icon: FileText, color: "text-blue-400" },
|
||||
{ name: "Comandos Shell", count: 6, icon: Terminal, color: "text-green-400" },
|
||||
{ name: "Web Research", count: 5, icon: Globe, color: "text-cyan-400" },
|
||||
{ name: "Knowledge Graph", count: 8, icon: Network, color: "text-amber-400" },
|
||||
{ name: "ERP & Database", count: 10, icon: Database, color: "text-emerald-400" },
|
||||
{ name: "Análise de Código", count: 7, icon: Code, color: "text-pink-400" },
|
||||
];
|
||||
|
||||
const uptime = metrics?.metrics?.uptime
|
||||
? `${Math.floor(metrics.metrics.uptime / 3600)}h ${Math.floor((metrics.metrics.uptime % 3600) / 60)}m`
|
||||
: "---";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] bg-[#0d0d1a] border-[#2a2a4a] text-white overflow-hidden p-0">
|
||||
<div className="sticky top-0 z-10 bg-gradient-to-r from-violet-600/20 via-purple-600/20 to-indigo-600/20 border-b border-violet-500/20 p-6">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 rounded-xl bg-gradient-to-br from-violet-500/30 to-purple-600/30 border border-violet-500/30">
|
||||
<Brain className="w-7 h-7 text-violet-400" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle className="text-xl font-bold text-white">Manus IA - Cérebro Central</DialogTitle>
|
||||
<DialogDescription className="text-violet-300/80">
|
||||
Arquitetura do motor de inteligência que alimenta todos os agentes
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-4 gap-3 mt-4">
|
||||
<div className="text-center p-3 rounded-lg bg-[#1a1a2e]/80 border border-[#2a2a4a]">
|
||||
<p className="text-2xl font-bold text-violet-400">{metrics?.model || "GPT-4o"}</p>
|
||||
<p className="text-[10px] text-gray-400 uppercase mt-1">Modelo</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-[#1a1a2e]/80 border border-[#2a2a4a]">
|
||||
<p className="text-2xl font-bold text-white">{metrics?.metrics?.totalCalls || 0}</p>
|
||||
<p className="text-[10px] text-gray-400 uppercase mt-1">Chamadas IA</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-[#1a1a2e]/80 border border-[#2a2a4a]">
|
||||
<p className="text-2xl font-bold text-emerald-400">{((metrics?.metrics?.totalTokens || 0) / 1000).toFixed(1)}k</p>
|
||||
<p className="text-[10px] text-gray-400 uppercase mt-1">Tokens</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-[#1a1a2e]/80 border border-[#2a2a4a]">
|
||||
<p className="text-2xl font-bold text-cyan-400">{uptime}</p>
|
||||
<p className="text-[10px] text-gray-400 uppercase mt-1">Uptime</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="max-h-[calc(90vh-220px)]">
|
||||
<div className="p-6 space-y-6">
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">
|
||||
<Layers className="w-4 h-4 text-violet-400" />
|
||||
Arquitetura: Fluxo de Inteligência
|
||||
</h3>
|
||||
<div className="relative p-4 rounded-xl bg-[#1a1a2e] border border-[#2a2a4a]">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-full p-3 rounded-lg bg-gradient-to-r from-violet-500/10 to-purple-500/10 border border-violet-500/20 text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-1">
|
||||
<Brain className="w-5 h-5 text-violet-400" />
|
||||
<span className="text-sm font-bold text-violet-300">ManusIntelligence (Singleton)</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400">GPT-4o + ToolManager + Context Enrichment</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<ArrowRight className="w-4 h-4 rotate-90" />
|
||||
<span className="text-[10px]">generate() / think()</span>
|
||||
<ArrowRight className="w-4 h-4 rotate-90" />
|
||||
</div>
|
||||
|
||||
<div className="w-full p-3 rounded-lg bg-[#0d0d1a] border border-[#1a1a3a] text-center">
|
||||
<span className="text-xs font-medium text-amber-400">enrichWithContext()</span>
|
||||
<p className="text-[10px] text-gray-500 mt-1">ToolManager.search_code → Contexto Semântico Automático</p>
|
||||
</div>
|
||||
|
||||
<ArrowRight className="w-4 h-4 rotate-90 text-gray-500" />
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 w-full">
|
||||
{AGENTS.map((agent) => (
|
||||
<div key={agent.name} className={`p-2.5 rounded-lg ${agent.bg} border border-[#2a2a4a] text-center`}>
|
||||
<agent.icon className={`w-4 h-4 ${agent.color} mx-auto mb-1`} />
|
||||
<p className={`text-xs font-semibold ${agent.color}`}>{agent.name}</p>
|
||||
<p className="text-[9px] text-gray-500 mt-0.5">{agent.role}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-amber-400" />
|
||||
Ferramentas Disponíveis ({metrics?.capabilities?.tools || 56})
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{TOOL_CATEGORIES.map((cat) => (
|
||||
<div key={cat.name} className="p-3 rounded-lg bg-[#1a1a2e] border border-[#2a2a4a] hover:border-[#3a3a5a] transition-all">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<cat.icon className={`w-3.5 h-3.5 ${cat.color}`} />
|
||||
<span className={`text-xs font-medium ${cat.color}`}>{cat.count}</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400">{cat.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-400" />
|
||||
Capacidades Ativas
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{[
|
||||
{ name: "Cérebro Central (GPT-4o)", active: true },
|
||||
{ name: "Enriquecimento de Contexto", active: true },
|
||||
{ name: "Busca Semântica de Código", active: true },
|
||||
{ name: "Knowledge Graph", active: true },
|
||||
{ name: "Pipeline Autônomo de Dev", active: true },
|
||||
{ name: "Orquestração de 6 Agentes", active: true },
|
||||
{ name: "Leitura/Escrita de Arquivos", active: true },
|
||||
{ name: "Execução de Comandos Shell", active: true },
|
||||
{ name: "Web Research", active: true },
|
||||
{ name: "Análise de Código", active: true },
|
||||
{ name: "Validação TypeScript", active: true },
|
||||
{ name: "Memória Evolutiva", active: true },
|
||||
].map((cap) => (
|
||||
<div key={cap.name} className="flex items-center gap-2 p-2 rounded-lg bg-[#1a1a2e] border border-[#2a2a4a]">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
||||
<span className="text-xs text-gray-300">{cap.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{metrics?.metrics?.errorCount > 0 && (
|
||||
<div className="p-3 rounded-lg bg-red-500/5 border border-red-500/20">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<AlertCircle className="w-4 h-4 text-red-400" />
|
||||
<span className="text-xs font-medium text-red-400">Erros Registrados</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">
|
||||
Total: <span className="text-red-400 font-medium">{metrics.metrics.errorCount}</span>
|
||||
{metrics.metrics.lastCallAt && (
|
||||
<> | Última chamada: <span className="text-gray-300">{new Date(metrics.metrics.lastCallAt).toLocaleString("pt-BR")}</span></>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 rounded-xl bg-[#1a1a2e] border border-[#2a2a4a]">
|
||||
<h3 className="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">
|
||||
<Network className="w-4 h-4 text-violet-400" />
|
||||
Diagrama de Fluxo
|
||||
</h3>
|
||||
<div className="font-mono text-[11px] text-gray-400 space-y-0.5 bg-[#0a0a15] p-4 rounded-lg border border-[#1a1a3a]">
|
||||
<p className="text-violet-400">{"┌──────────────────────────────────────────────────────────┐"}</p>
|
||||
<p className="text-violet-400">{"│ MANUS INTELLIGENCE (Singleton GPT-4o) │"}</p>
|
||||
<p className="text-violet-400">{"├──────────────────────────────────────────────────────────┤"}</p>
|
||||
<p>{"│ .generate(prompt) → enrichWithContext() → OpenAI │"}</p>
|
||||
<p>{"│ .think(prompt) → enrichWithContext() → OpenAI │"}</p>
|
||||
<p>{"│ .getMetrics() → calls, tokens, errors, uptime │"}</p>
|
||||
<p className="text-violet-400">{"├──────────────────────────────────────────────────────────┤"}</p>
|
||||
<p className="text-amber-400">{"│ ToolManager.executeTool('search_code', query) │"}</p>
|
||||
<p className="text-amber-400">{"│ → Contexto semântico injetado automaticamente │"}</p>
|
||||
<p className="text-violet-400">{"├──────────────────────────────────────────────────────────┤"}</p>
|
||||
<p className="text-blue-400">{"│ Architect ──────┐ │"}</p>
|
||||
<p className="text-green-400">{"│ Generator ──────┤ │"}</p>
|
||||
<p className="text-amber-400">{"│ Validator ──────┤── Todos via manusIntelligence ──► │"}</p>
|
||||
<p className="text-red-400">{"│ Executor ──────┤ │"}</p>
|
||||
<p className="text-cyan-400">{"│ Researcher ──────┤ │"}</p>
|
||||
<p className="text-purple-400">{"│ Evolution ──────┘ │"}</p>
|
||||
<p className="text-violet-400">{"├──────────────────────────────────────────────────────────┤"}</p>
|
||||
<p className="text-emerald-400">{"│ /api/manus/health → Status, métricas, capacidades │"}</p>
|
||||
<p className="text-violet-400">{"└──────────────────────────────────────────────────────────┘"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentCard({ agent }: { agent: AgentStatus }) {
|
||||
return (
|
||||
<div data-testid={`agent-card-${agent.name}`} className="flex items-center justify-between p-3 rounded-lg bg-[#1a1a2e] border border-[#2a2a4a]">
|
||||
|
|
@ -442,7 +209,6 @@ function SummaryCards({ summary }: { summary: EngineRoomData["summary"] }) {
|
|||
export default function EngineRoom() {
|
||||
const queryClient = useQueryClient();
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [manusOpen, setManusOpen] = useState(false);
|
||||
|
||||
const { data, isLoading, isRefetching } = useQuery<EngineRoomData>({
|
||||
queryKey: ["/api/engine-room/status"],
|
||||
|
|
@ -461,12 +227,6 @@ export default function EngineRoom() {
|
|||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const { data: manusMetrics } = useQuery<any>({
|
||||
queryKey: ["/api/manus/health"],
|
||||
enabled: activeTab === "manus" || manusOpen,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const handleRefresh = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/engine-room/status"] });
|
||||
};
|
||||
|
|
@ -537,9 +297,6 @@ export default function EngineRoom() {
|
|||
<TabsTrigger value="automation" data-testid="tab-automation" className="data-[state=active]:bg-purple-500/10 data-[state=active]:text-purple-400">
|
||||
<Zap className="w-4 h-4 mr-1.5" /> Motor Automacao
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="manus" data-testid="tab-manus" className="data-[state=active]:bg-violet-500/10 data-[state=active]:text-violet-400">
|
||||
<Brain className="w-4 h-4 mr-1.5" /> Manus IA
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="agents" data-testid="tab-agents" className="data-[state=active]:bg-cyan-500/10 data-[state=active]:text-cyan-400">
|
||||
<Bot className="w-4 h-4 mr-1.5" /> Agentes XOS
|
||||
</TabsTrigger>
|
||||
|
|
@ -548,11 +305,7 @@ export default function EngineRoom() {
|
|||
<TabsContent value="overview">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{data.engines.map((engine) => (
|
||||
<EngineCard
|
||||
key={engine.name}
|
||||
engine={engine}
|
||||
onClick={engine.name === "manus-ia" ? () => setManusOpen(true) : undefined}
|
||||
/>
|
||||
<EngineCard key={engine.name} engine={engine} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
|
@ -685,83 +438,6 @@ export default function EngineRoom() {
|
|||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manus">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{data.engines.filter(e => e.name === "manus-ia").map(e => (
|
||||
<EngineCard key={e.name} engine={e} onClick={() => setManusOpen(true)} />
|
||||
))}
|
||||
<Card className="bg-[#1a1a2e] border-[#2a2a4a]">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-violet-400" /> Metricas do Manus IA
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{manusMetrics ? (
|
||||
<>
|
||||
<div className="p-3 rounded bg-[#0d0d1a] border border-[#1a1a3a]">
|
||||
<p className="text-xs text-gray-500 uppercase mb-2">Modelo & Performance</p>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-violet-400">{manusMetrics.model || "GPT-4o"}</p>
|
||||
<p className="text-[10px] text-gray-500">Modelo</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-white">{manusMetrics.metrics?.totalCalls || 0}</p>
|
||||
<p className="text-[10px] text-gray-500">Chamadas IA</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-emerald-400">{((manusMetrics.metrics?.totalTokens || 0) / 1000).toFixed(1)}k</p>
|
||||
<p className="text-[10px] text-gray-500">Tokens</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded bg-[#0d0d1a] border border-[#1a1a3a]">
|
||||
<p className="text-xs text-gray-500 uppercase mb-2">Capacidades Ativas</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-bold text-white">{manusMetrics.capabilities?.tools || 56}</span>
|
||||
<span className="text-xs text-gray-400">Ferramentas</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-bold text-white">{manusMetrics.capabilities?.agents || 0}</span>
|
||||
<span className="text-xs text-gray-400">Agentes Ativos</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded bg-[#0d0d1a] border border-[#1a1a3a]">
|
||||
<p className="text-xs text-gray-500 uppercase mb-2">Uptime</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
{manusMetrics.metrics?.uptime
|
||||
? `${Math.floor(manusMetrics.metrics.uptime / 3600)}h ${Math.floor((manusMetrics.metrics.uptime % 3600) / 60)}m`
|
||||
: "---"}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-4 text-gray-500">
|
||||
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-2" />
|
||||
<p className="text-xs">Carregando metricas...</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="mt-4 p-4 rounded-lg bg-[#1a1a2e] border border-[#2a2a4a]">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-2 flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-violet-400" /> Capacidades do Manus IA
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{["GPT-4o (Cerebro Central)", "56 Ferramentas Integradas", "Knowledge Graph", "Busca Semantica", "Pipeline de Dev Autonomo", "Orquestrador de Agentes", "Leitura/Escrita de Arquivos", "Execucao de Comandos", "Web Research", "Analise de Codigo", "Validacao TypeScript", "Memoria Evolutiva"].map(cap => (
|
||||
<div key={cap} className="flex items-center gap-2 p-2 rounded bg-[#0d0d1a]">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-violet-400 shrink-0" />
|
||||
<span className="text-xs text-gray-300">{cap}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="agents">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||||
|
|
@ -813,15 +489,13 @@ export default function EngineRoom() {
|
|||
<p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p>
|
||||
<p>{"│ Express.js (5000) ─── Orquestracao + API Gateway │"}</p>
|
||||
<p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p>
|
||||
<p className="text-violet-400 font-bold">{"│ Manus IA (5000) ─── GPT-4o + 56 Tools (Cerebro)│"}</p>
|
||||
<p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p>
|
||||
<p className="text-blue-400">{"│ Plus ERP (8080) ─── Laravel/PHP - ERP Completo │"}</p>
|
||||
<p className="text-amber-300">{"│ Contabil (8003) ─── FastAPI - DRE/Balancete │"}</p>
|
||||
<p className="text-amber-300">{"│ Fiscal (8002) ─── FastAPI - NF-e/SEFAZ │"}</p>
|
||||
<p className="text-emerald-400">{"│ BI Engine (8004) ─── FastAPI - SQL/Charts/Cache │"}</p>
|
||||
<p className="text-purple-400">{"│ Automacao (8005) ─── FastAPI - Scheduler/Events │"}</p>
|
||||
<p className="text-amber-400">{"├─────────────────────────────────────────────────────┤"}</p>
|
||||
<p className="text-cyan-400">{"│ XOS Agents ─── 6 Agentes via ManusIntel. │"}</p>
|
||||
<p className="text-cyan-400">{"│ XOS Agents ─── 6 Agentes Autonomos │"}</p>
|
||||
<p className="text-amber-400">{"└─────────────────────────────────────────────────────┘"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -832,8 +506,6 @@ export default function EngineRoom() {
|
|||
<p>Nao foi possivel carregar o status dos motores</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ManusStructurePanel open={manusOpen} onOpenChange={setManusOpen} metrics={manusMetrics} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -137,12 +137,12 @@ export default function EngineeringHub() {
|
|||
{isErpConnected ? (
|
||||
<Badge className="bg-green-100 text-green-700 border-green-300 flex items-center gap-1.5 px-3 py-1.5">
|
||||
<Link2 className="h-3.5 w-3.5" />
|
||||
Sincronizado com Arcádia SOE
|
||||
Sincronizado com Arcádia ERP
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="flex items-center gap-1.5 px-3 py-1.5">
|
||||
<AlertTriangle className="h-3.5 w-3.5" />
|
||||
Arcádia SOE não conectado
|
||||
Arcádia ERP não conectado
|
||||
</Badge>
|
||||
)}
|
||||
<Button variant="outline" size="sm" data-testid="btn-refresh-engineering">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -41,7 +40,7 @@ import {
|
|||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
|
||||
type TabType = "dashboard" | "payables" | "receivables" | "accounts" | "transactions" | "settings" | "analise";
|
||||
type TabType = "dashboard" | "payables" | "receivables" | "accounts" | "transactions" | "settings";
|
||||
|
||||
interface BankAccount {
|
||||
id: number;
|
||||
|
|
@ -976,14 +975,13 @@ export default function Financeiro() {
|
|||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)} className="space-y-4">
|
||||
<TabsList className="grid grid-cols-7 w-full max-w-4xl">
|
||||
<TabsList className="grid grid-cols-6 w-full max-w-4xl">
|
||||
<TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger>
|
||||
<TabsTrigger value="payables" data-testid="tab-payables">A Pagar</TabsTrigger>
|
||||
<TabsTrigger value="receivables" data-testid="tab-receivables">A Receber</TabsTrigger>
|
||||
<TabsTrigger value="accounts" data-testid="tab-accounts">Contas</TabsTrigger>
|
||||
<TabsTrigger value="transactions" data-testid="tab-transactions">Extrato</TabsTrigger>
|
||||
<TabsTrigger value="settings" data-testid="tab-settings">Configurações</TabsTrigger>
|
||||
<TabsTrigger value="analise" data-testid="tab-analise">Análise BI</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="dashboard">{renderDashboard()}</TabsContent>
|
||||
|
|
@ -992,9 +990,6 @@ export default function Financeiro() {
|
|||
<TabsContent value="accounts">{renderAccounts()}</TabsContent>
|
||||
<TabsContent value="transactions">{renderTransactions()}</TabsContent>
|
||||
<TabsContent value="settings">{renderSettings()}</TabsContent>
|
||||
<TabsContent value="analise">
|
||||
<SupersetDashboard dashboardId="financial-overview" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Dialog open={showAccountDialog} onOpenChange={setShowAccountDialog}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
||||
import {
|
||||
Search, Grid, Settings, FileText, Plus, Star,
|
||||
ChevronRight, Bell, Clock, CheckSquare,
|
||||
|
|
@ -120,7 +119,7 @@ export default function Home() {
|
|||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [editingApp, setEditingApp] = useState<Application | null>(null);
|
||||
const [newNoteContent, setNewNoteContent] = useState("");
|
||||
const isAdmin = user?.role === "admin" || user?.role === "master";
|
||||
const isAdmin = user?.role === "admin";
|
||||
const [newApp, setNewApp] = useState({
|
||||
name: "",
|
||||
category: "",
|
||||
|
|
@ -1171,15 +1170,6 @@ export default function Home() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arcádia Insights — Dashboard executivo */}
|
||||
<div className="mt-6 px-6 pb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-semibold text-slate-800">Arcádia Insights</h2>
|
||||
<a href="/bi" className="text-xs text-indigo-500 hover:underline">Abrir BI completo →</a>
|
||||
</div>
|
||||
<SupersetDashboard dashboardId="executive-summary" height={400} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export default function Migration() {
|
|||
<div className="mt-2 border-2 border-dashed rounded-lg p-6 text-center">
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip,.json,.csv,.rar,.sql,.sql.gz"
|
||||
accept=".zip,.json,.csv"
|
||||
className="hidden"
|
||||
id="backup-file"
|
||||
onChange={e => setUploadFile(e.target.files?.[0] || null)}
|
||||
|
|
@ -286,7 +286,7 @@ export default function Migration() {
|
|||
<p className="mt-2 text-sm text-gray-500">
|
||||
{uploadFile ? uploadFile.name : "Clique para selecionar"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">ZIP (MongoDB), RAR, SQL, SQL.GZ, JSON ou CSV</p>
|
||||
<p className="text-xs text-gray-400 mt-1">ZIP (MongoDB), JSON ou CSV</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1855,7 +1855,7 @@ export default function ProcessCompass() {
|
|||
data-testid="search-projects"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={() => setShowNewProjectDialog(true)} data-testid="btn-new-project">
|
||||
<Button onClick={() => setShowNewProjectDialog(true)} disabled={clients.length === 0} data-testid="btn-new-project">
|
||||
<Plus className="h-4 w-4 mr-2" /> Novo Projeto
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -4600,32 +4600,18 @@ export default function ProcessCompass() {
|
|||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="project-client">Cliente</Label>
|
||||
{clients.length === 0 ? (
|
||||
<div className="rounded-md border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-800">
|
||||
Nenhum cliente cadastrado.{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="underline font-medium"
|
||||
onClick={() => { setShowNewProjectDialog(false); setShowNewClientDialog(true); }}
|
||||
>
|
||||
Criar cliente
|
||||
</button>{" "}
|
||||
antes de criar um projeto.
|
||||
</div>
|
||||
) : (
|
||||
<Select name="clientId" required>
|
||||
<SelectTrigger data-testid="select-project-client">
|
||||
<SelectValue placeholder="Selecione um cliente" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{clients.map(client => (
|
||||
<SelectItem key={client.id} value={client.id.toString()}>
|
||||
{client.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<Select name="clientId" required>
|
||||
<SelectTrigger data-testid="select-project-client">
|
||||
<SelectValue placeholder="Selecione um cliente" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{clients.map(client => (
|
||||
<SelectItem key={client.id} value={client.id.toString()}>
|
||||
{client.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="project-description">Descrição</Label>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,8 @@ import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
|||
import {
|
||||
Users, Building2, TrendingUp, MessageSquare, Ticket, Zap, LayoutGrid,
|
||||
ChevronRight, PlusCircle, Filter, Search, Bell, Calendar, Target,
|
||||
BarChart3, DollarSign, Clock, AlertCircle, Phone, Mail, ArrowUpRight
|
||||
BarChart3, DollarSign, Clock, AlertCircle, Phone, Mail, ArrowUpRight,
|
||||
Activity, Hash, Shield
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
|
|
@ -15,6 +16,7 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
interface XosStats {
|
||||
total_contacts: number;
|
||||
|
|
@ -69,12 +71,10 @@ interface Activity {
|
|||
export default function XosCentral() {
|
||||
const [activeTab, setActiveTab] = useState("dashboard");
|
||||
const [isNewContactOpen, setIsNewContactOpen] = useState(false);
|
||||
const [newContact, setNewContact] = useState({ name: "", email: "", phone: "", company: "", position: "" });
|
||||
const [isNewActivityOpen, setIsNewActivityOpen] = useState(false);
|
||||
const [newActivity, setNewActivity] = useState({ type: "task", title: "", description: "", due_at: "", priority: "normal" });
|
||||
const [isNewSelectorOpen, setIsNewSelectorOpen] = useState(false);
|
||||
const [isNewDealOpen, setIsNewDealOpen] = useState(false);
|
||||
const [newDeal, setNewDeal] = useState({ title: "", pipeline_id: "1", stage_id: "1", value: "" });
|
||||
const [newContact, setNewContact] = useState({ name: "", email: "", phone: "", company: "", position: "" });
|
||||
const [newActivity, setNewActivity] = useState({ type: "task", title: "", due_at: "", priority: "medium" });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createContactMutation = useMutation({
|
||||
|
|
@ -95,24 +95,6 @@ export default function XosCentral() {
|
|||
},
|
||||
});
|
||||
|
||||
const createDealMutation = useMutation({
|
||||
mutationFn: async (data: typeof newDeal) => {
|
||||
const res = await fetch("/api/xos/deals", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) throw new Error("Erro ao criar negócio");
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/xos/deals"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] });
|
||||
setIsNewDealOpen(false);
|
||||
setNewDeal({ title: "", pipeline_id: "1", stage_id: "1", value: "" });
|
||||
},
|
||||
});
|
||||
|
||||
const createActivityMutation = useMutation({
|
||||
mutationFn: async (data: typeof newActivity) => {
|
||||
const res = await fetch("/api/xos/activities", {
|
||||
|
|
@ -127,7 +109,7 @@ export default function XosCentral() {
|
|||
queryClient.invalidateQueries({ queryKey: ["/api/xos/activities"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] });
|
||||
setIsNewActivityOpen(false);
|
||||
setNewActivity({ type: "task", title: "", description: "", due_at: "", priority: "normal" });
|
||||
setNewActivity({ type: "task", title: "", due_at: "", priority: "medium" });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -161,6 +143,9 @@ export default function XosCentral() {
|
|||
{ id: "automations", name: "Automações", icon: Zap, href: "/xos/automations", color: "bg-violet-100 text-violet-600", description: "Workflows automáticos" },
|
||||
{ id: "campaigns", name: "Campanhas", icon: Target, href: "/xos/campaigns", color: "bg-pink-100 text-pink-600", description: "Marketing automation" },
|
||||
{ id: "sites", name: "Sites", icon: LayoutGrid, href: "/xos/sites", color: "bg-cyan-100 text-cyan-600", description: "Site builder" },
|
||||
{ id: "supervisor", name: "Supervisor", icon: Activity, href: "/xos/supervisor", color: "bg-indigo-100 text-indigo-600", description: "Monitor em tempo real" },
|
||||
{ id: "reports", name: "Relatórios", icon: BarChart3, href: "/xos/reports", color: "bg-emerald-100 text-emerald-600", description: "CSAT, SLA e KPIs" },
|
||||
{ id: "protocols", name: "Protocolos", icon: Hash, href: "/xos/protocols", color: "bg-teal-100 text-teal-600", description: "Rastreamento de atendimentos" },
|
||||
];
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
|
|
@ -241,9 +226,9 @@ export default function XosCentral() {
|
|||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button data-testid="button-new-contact" onClick={() => setIsNewSelectorOpen(true)}>
|
||||
<Button data-testid="button-new-contact" onClick={() => setIsNewContactOpen(true)}>
|
||||
<PlusCircle className="h-4 w-4 mr-2" />
|
||||
Novo
|
||||
Novo Contato
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -328,7 +313,7 @@ export default function XosCentral() {
|
|||
{/* Modules Grid */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-800 mb-4">Módulos XOS</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-9 gap-4">
|
||||
{modules.map((mod) => (
|
||||
<Link key={mod.id} href={mod.href}>
|
||||
<Card className="hover:shadow-lg transition-all cursor-pointer group" data-testid={`card-module-${mod.id}`}>
|
||||
|
|
@ -584,175 +569,129 @@ export default function XosCentral() {
|
|||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Seletor: o que deseja criar? */}
|
||||
<Dialog open={isNewSelectorOpen} onOpenChange={setIsNewSelectorOpen}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>O que deseja criar?</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3 pt-2">
|
||||
<button
|
||||
className="flex items-center gap-4 p-4 rounded-lg border hover:bg-blue-50 hover:border-blue-300 transition-colors text-left"
|
||||
onClick={() => { setIsNewSelectorOpen(false); setIsNewContactOpen(true); }}
|
||||
>
|
||||
<div className="bg-blue-100 p-2 rounded-lg"><Users className="h-5 w-5 text-blue-600" /></div>
|
||||
<div><p className="font-semibold text-slate-800">Contato</p><p className="text-sm text-slate-500">Adicionar lead ou cliente</p></div>
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-4 p-4 rounded-lg border hover:bg-emerald-50 hover:border-emerald-300 transition-colors text-left"
|
||||
onClick={() => { setIsNewSelectorOpen(false); setIsNewDealOpen(true); }}
|
||||
>
|
||||
<div className="bg-emerald-100 p-2 rounded-lg"><TrendingUp className="h-5 w-5 text-emerald-600" /></div>
|
||||
<div><p className="font-semibold text-slate-800">Negócio</p><p className="text-sm text-slate-500">Criar oportunidade de venda</p></div>
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-4 p-4 rounded-lg border hover:bg-violet-50 hover:border-violet-300 transition-colors text-left"
|
||||
onClick={() => { setIsNewSelectorOpen(false); setIsNewActivityOpen(true); }}
|
||||
>
|
||||
<div className="bg-violet-100 p-2 rounded-lg"><Calendar className="h-5 w-5 text-violet-600" /></div>
|
||||
<div><p className="font-semibold text-slate-800">Atividade</p><p className="text-sm text-slate-500">Agendar tarefa ou reunião</p></div>
|
||||
</button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Dialog Novo Negócio */}
|
||||
<Dialog open={isNewDealOpen} onOpenChange={setIsNewDealOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Novo Negócio</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div>
|
||||
<Label>Título *</Label>
|
||||
<Input placeholder="Ex: Proposta comercial Empresa X" value={newDeal.title} onChange={(e) => setNewDeal({ ...newDeal, title: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Estágio</Label>
|
||||
<select className="w-full mt-1 border rounded-md px-3 py-2 text-sm" value={newDeal.stage_id} onChange={(e) => setNewDeal({ ...newDeal, stage_id: e.target.value })}>
|
||||
<option value="1">Prospecção</option>
|
||||
<option value="2">Qualificação</option>
|
||||
<option value="3">Proposta</option>
|
||||
<option value="4">Negociação</option>
|
||||
<option value="5">Ganho</option>
|
||||
<option value="6">Perdido</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Valor (R$)</Label>
|
||||
<Input type="number" placeholder="0,00" value={newDeal.value} onChange={(e) => setNewDeal({ ...newDeal, value: e.target.value })} />
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" className="flex-1" onClick={() => setIsNewDealOpen(false)}>Cancelar</Button>
|
||||
{/* Modal: Novo Contato */}
|
||||
<Dialog open={isNewContactOpen} onOpenChange={setIsNewContactOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Novo Contato</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Nome *</Label>
|
||||
<Input
|
||||
value={newContact.name}
|
||||
onChange={(e) => setNewContact({ ...newContact, name: e.target.value })}
|
||||
placeholder="Nome do contato"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
value={newContact.email}
|
||||
onChange={(e) => setNewContact({ ...newContact, email: e.target.value })}
|
||||
placeholder="email@exemplo.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Telefone</Label>
|
||||
<Input
|
||||
value={newContact.phone}
|
||||
onChange={(e) => setNewContact({ ...newContact, phone: e.target.value })}
|
||||
placeholder="(00) 00000-0000"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Empresa</Label>
|
||||
<Input
|
||||
value={newContact.company}
|
||||
onChange={(e) => setNewContact({ ...newContact, company: e.target.value })}
|
||||
placeholder="Empresa"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Cargo</Label>
|
||||
<Input
|
||||
value={newContact.position}
|
||||
onChange={(e) => setNewContact({ ...newContact, position: e.target.value })}
|
||||
placeholder="Cargo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => createDealMutation.mutate(newDeal)}
|
||||
disabled={!newDeal.title || createDealMutation.isPending}
|
||||
>
|
||||
{createDealMutation.isPending ? "Salvando..." : "Salvar Negócio"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isNewContactOpen} onOpenChange={setIsNewContactOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Novo Contato</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div>
|
||||
<Label>Nome *</Label>
|
||||
<Input placeholder="Nome completo" value={newContact.name} onChange={(e) => setNewContact({ ...newContact, name: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>E-mail</Label>
|
||||
<Input placeholder="email@empresa.com" value={newContact.email} onChange={(e) => setNewContact({ ...newContact, email: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Telefone / WhatsApp</Label>
|
||||
<Input placeholder="(11) 99999-9999" value={newContact.phone} onChange={(e) => setNewContact({ ...newContact, phone: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Empresa</Label>
|
||||
<Input placeholder="Nome da empresa" value={newContact.company} onChange={(e) => setNewContact({ ...newContact, company: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Cargo</Label>
|
||||
<Input placeholder="Cargo ou função" value={newContact.position} onChange={(e) => setNewContact({ ...newContact, position: e.target.value })} />
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" className="flex-1" onClick={() => setIsNewContactOpen(false)}>Cancelar</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
className="w-full"
|
||||
onClick={() => createContactMutation.mutate(newContact)}
|
||||
disabled={!newContact.name || createContactMutation.isPending}
|
||||
>
|
||||
{createContactMutation.isPending ? "Salvando..." : "Salvar Contato"}
|
||||
{createContactMutation.isPending ? "Salvando..." : "Criar Contato"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isNewActivityOpen} onOpenChange={setIsNewActivityOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nova Atividade</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div>
|
||||
<Label>Tipo</Label>
|
||||
<select
|
||||
className="w-full mt-1 border rounded-md px-3 py-2 text-sm"
|
||||
value={newActivity.type}
|
||||
onChange={(e) => setNewActivity({ ...newActivity, type: e.target.value })}
|
||||
>
|
||||
<option value="task">Tarefa</option>
|
||||
<option value="call">Ligação</option>
|
||||
<option value="email">E-mail</option>
|
||||
<option value="meeting">Reunião</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Título *</Label>
|
||||
<Input placeholder="Descreva a atividade" value={newActivity.title} onChange={(e) => setNewActivity({ ...newActivity, title: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Descrição</Label>
|
||||
<Input placeholder="Detalhes adicionais" value={newActivity.description} onChange={(e) => setNewActivity({ ...newActivity, description: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Data prevista</Label>
|
||||
<Input type="datetime-local" value={newActivity.due_at} onChange={(e) => setNewActivity({ ...newActivity, due_at: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Prioridade</Label>
|
||||
<select
|
||||
className="w-full mt-1 border rounded-md px-3 py-2 text-sm"
|
||||
value={newActivity.priority}
|
||||
onChange={(e) => setNewActivity({ ...newActivity, priority: e.target.value })}
|
||||
>
|
||||
<option value="low">Baixa</option>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="high">Alta</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" className="flex-1" onClick={() => setIsNewActivityOpen(false)}>Cancelar</Button>
|
||||
{/* Modal: Nova Atividade */}
|
||||
<Dialog open={isNewActivityOpen} onOpenChange={setIsNewActivityOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nova Atividade</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Título *</Label>
|
||||
<Input
|
||||
value={newActivity.title}
|
||||
onChange={(e) => setNewActivity({ ...newActivity, title: e.target.value })}
|
||||
placeholder="Descrição da atividade"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Tipo</Label>
|
||||
<Select value={newActivity.type} onValueChange={(v) => setNewActivity({ ...newActivity, type: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="task">Tarefa</SelectItem>
|
||||
<SelectItem value="call">Ligação</SelectItem>
|
||||
<SelectItem value="email">E-mail</SelectItem>
|
||||
<SelectItem value="meeting">Reunião</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Prioridade</Label>
|
||||
<Select value={newActivity.priority} onValueChange={(v) => setNewActivity({ ...newActivity, priority: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Baixa</SelectItem>
|
||||
<SelectItem value="medium">Média</SelectItem>
|
||||
<SelectItem value="high">Alta</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Data prevista</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={newActivity.due_at}
|
||||
onChange={(e) => setNewActivity({ ...newActivity, due_at: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="flex-1"
|
||||
className="w-full"
|
||||
onClick={() => createActivityMutation.mutate(newActivity)}
|
||||
disabled={!newActivity.title || createActivityMutation.isPending}
|
||||
>
|
||||
{createActivityMutation.isPending ? "Salvando..." : "Salvar Atividade"}
|
||||
{createActivityMutation.isPending ? "Salvando..." : "Criar Atividade"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</BrowserFrame>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
|
|||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
const browserIcon = "/arcadia_suite_icon.png";
|
||||
import browserIcon from "/favicon.png";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export default function AuthPage() {
|
||||
|
|
|
|||
|
|
@ -77,15 +77,6 @@ services:
|
|||
ERPNEXT_URL: ${ERPNEXT_URL:-http://erpnext:8080}
|
||||
ERPNEXT_API_KEY: ${ERPNEXT_API_KEY:-}
|
||||
ERPNEXT_API_SECRET: ${ERPNEXT_API_SECRET:-}
|
||||
# ── MiroFlow (container) ─────────────────────────────────────────────
|
||||
MIROFLOW_HOST: "miroflow"
|
||||
MIROFLOW_PORT: "8006"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5000/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
|
|
@ -151,6 +142,38 @@ services:
|
|||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── Apache Superset (BI) ─────────────────────────────────────────────────────
|
||||
# Perfil `bi` — suba com: docker compose --profile bi up
|
||||
superset:
|
||||
image: apache/superset:4.1.0
|
||||
restart: always
|
||||
profiles: [bi]
|
||||
environment:
|
||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/arcadia_superset
|
||||
ARCADIA_DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
|
||||
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
|
||||
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
|
||||
SUPERSET_WEBSERVER_PORT: "8088"
|
||||
PYTHONPATH: /app/pythonpath
|
||||
volumes:
|
||||
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
|
||||
- ./docker/superset/init.sh:/app/docker/init.sh:ro
|
||||
- superset_home:/app/superset_home
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
command: ["/bin/bash", "/app/docker/init.sh"]
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── ERPNext (Frappe Framework) ───────────────────────────────────────────────
|
||||
# Perfil `erpnext` — suba com: docker compose --profile erpnext up
|
||||
erpnext-db:
|
||||
|
|
@ -203,19 +226,135 @@ services:
|
|||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
|
||||
# ── MiroFlow (agente científico) ─────────────────────────────────────────────
|
||||
miroflow:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.miroflow
|
||||
# ── Arcádia Plus (Laravel + MySQL) ──────────────────────────────────────────
|
||||
# Perfil `plus` — suba com: docker compose --profile plus up
|
||||
plus-db:
|
||||
image: mysql:8.0
|
||||
restart: always
|
||||
profiles: [plus]
|
||||
environment:
|
||||
MIROFLOW_PORT: "8006"
|
||||
OLLAMA_BASE_URL: "http://ollama-ia1upsekrad96at5hq97e4qa:11434"
|
||||
MYSQL_ROOT_PASSWORD: ${PLUS_DB_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${PLUS_DB_DATABASE:-arcadia_plus}
|
||||
MYSQL_USER: ${PLUS_DB_USER:-plus}
|
||||
MYSQL_PASSWORD: ${PLUS_DB_PASSWORD}
|
||||
volumes:
|
||||
- plus_db:/var/lib/mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=${PLUS_DB_ROOT_PASSWORD}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
plus:
|
||||
image: ${PLUS_IMAGE:-php:8.3-apache}
|
||||
restart: always
|
||||
profiles: [plus]
|
||||
environment:
|
||||
APP_ENV: production
|
||||
APP_KEY: ${PLUS_APP_KEY}
|
||||
APP_URL: https://${DOMAIN}/plus
|
||||
DB_HOST: plus-db
|
||||
DB_DATABASE: ${PLUS_DB_DATABASE:-arcadia_plus}
|
||||
DB_USERNAME: ${PLUS_DB_USER:-plus}
|
||||
DB_PASSWORD: ${PLUS_DB_PASSWORD}
|
||||
SESSION_DRIVER: redis
|
||||
REDIS_HOST: redis
|
||||
ARCADIA_URL: https://${DOMAIN}
|
||||
ARCADIA_SSO_SECRET: ${SSO_SECRET}
|
||||
volumes:
|
||||
- plus_storage:/var/www/html/storage
|
||||
depends_on:
|
||||
plus-db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── Apache Superset (BI) ─────────────────────────────────────────────────────
|
||||
# Perfil `bi` — suba com: docker compose --profile bi up
|
||||
superset:
|
||||
image: apache/superset:4.1.0
|
||||
restart: always
|
||||
profiles: [bi]
|
||||
environment:
|
||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/arcadia_superset
|
||||
ARCADIA_DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
|
||||
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
|
||||
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
|
||||
SUPERSET_WEBSERVER_PORT: "8088"
|
||||
PYTHONPATH: /app/pythonpath
|
||||
volumes:
|
||||
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
|
||||
- ./docker/superset/init.sh:/app/docker/init.sh:ro
|
||||
- superset_home:/app/superset_home
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
command: ["/bin/bash", "/app/docker/init.sh"]
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── ERPNext (Frappe Framework) ───────────────────────────────────────────────
|
||||
# Perfil `erpnext` — suba com: docker compose --profile erpnext up
|
||||
erpnext-db:
|
||||
image: mariadb:10.6
|
||||
restart: always
|
||||
profiles: [erpnext]
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${ERPNEXT_DB_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: _frappe
|
||||
MYSQL_USER: frappe
|
||||
MYSQL_PASSWORD: ${ERPNEXT_DB_PASSWORD}
|
||||
volumes:
|
||||
- erpnext_db:/var/lib/mysql
|
||||
command: >
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_unicode_ci
|
||||
--skip-character-set-client-handshake
|
||||
--skip-innodb-read-only-compressed
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=${ERPNEXT_DB_ROOT_PASSWORD}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
erpnext:
|
||||
image: frappe/erpnext:version-15
|
||||
restart: always
|
||||
profiles: [erpnext]
|
||||
environment:
|
||||
FRAPPE_SITE_NAME_HEADER: erpnext.local
|
||||
ERPNEXT_DB_ROOT_PASSWORD: ${ERPNEXT_DB_ROOT_PASSWORD}
|
||||
ERPNEXT_ADMIN_PASSWORD: ${ERPNEXT_ADMIN_PASSWORD}
|
||||
volumes:
|
||||
- erpnext_sites:/home/frappe/frappe-bench/sites
|
||||
- erpnext_logs:/home/frappe/frappe-bench/logs
|
||||
- ./docker/erpnext/init.sh:/usr/local/bin/init-erpnext.sh:ro
|
||||
depends_on:
|
||||
erpnext-db:
|
||||
condition: service_healthy
|
||||
command: ["/bin/bash", "/usr/local/bin/init-erpnext.sh"]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:8080/api/method/frappe.ping || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 120s
|
||||
networks:
|
||||
- arcadia-internal
|
||||
- ollama-net
|
||||
|
||||
# ── Microserviços Python ─────────────────────────────────────────────────────
|
||||
contabil:
|
||||
|
|
@ -327,7 +466,6 @@ services:
|
|||
networks:
|
||||
- arcadia-internal
|
||||
- coolify
|
||||
- ollama-net
|
||||
|
||||
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
|
||||
# OPÇÃO A (padrão): Ollama como container Docker
|
||||
|
|
@ -377,10 +515,8 @@ services:
|
|||
environment:
|
||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
|
||||
SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset
|
||||
PYTHONPATH: /app/pythonpath
|
||||
SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USERNAME:-admin}
|
||||
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USERNAME:-admin}
|
||||
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
|
||||
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@onboardbi.com.br}
|
||||
ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
|
|
@ -388,11 +524,6 @@ services:
|
|||
volumes:
|
||||
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
|
||||
- ./docker/superset/init.sh:/app/docker/init.sh:ro
|
||||
- ./docker/superset/images/arcadia_logo.png:/app/superset/static/assets/images/arcadia_logo.png:ro
|
||||
- ./docker/superset/images/arcadia_logo_dark.png:/app/superset/static/assets/images/arcadia_logo_dark.png:ro
|
||||
- ./docker/superset/images/superset-logo-horiz.png:/app/superset/static/assets/images/superset-logo-horiz.png:ro
|
||||
- ./docker/superset/images/favicon.png:/app/superset/static/assets/images/favicon.png:ro
|
||||
- ./docker/superset/images/arcadia_favicon.png:/app/superset/static/assets/images/arcadia_favicon.png:ro
|
||||
- superset_home:/app/superset_home
|
||||
depends_on:
|
||||
db:
|
||||
|
|
@ -408,48 +539,22 @@ services:
|
|||
- "traefik.http.routers.superset.tls=true"
|
||||
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.superset.loadbalancer.server.port=8088"
|
||||
- "traefik.http.routers.superset-http.entrypoints=http"
|
||||
- "traefik.http.routers.superset-http.rule=Host(`${SUPERSET_DOMAIN:-bi.onboardbi.com.br}`)"
|
||||
- "traefik.http.routers.superset-http.middlewares=superset-https-redirect"
|
||||
- "traefik.http.routers.superset-http.service=superset"
|
||||
- "traefik.http.middlewares.superset-https-redirect.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.superset-https-redirect.redirectscheme.permanent=true"
|
||||
profiles: [bi]
|
||||
|
||||
# ── Neo4j (Knowledge Graph) ─────────────────────────────────────────────────
|
||||
# Ativar com: docker compose --profile kg up
|
||||
neo4j:
|
||||
image: neo4j:5
|
||||
restart: always
|
||||
environment:
|
||||
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-arcadia123}
|
||||
NEO4J_PLUGINS: '["apoc"]'
|
||||
volumes:
|
||||
- neo4j_data:/data
|
||||
- neo4j_logs:/logs
|
||||
ports:
|
||||
- "7474:7474"
|
||||
- "7687:7687"
|
||||
networks:
|
||||
- arcadia-internal
|
||||
profiles: [kg]
|
||||
|
||||
networks:
|
||||
arcadia-internal:
|
||||
driver: bridge
|
||||
coolify:
|
||||
external: true
|
||||
ollama-net:
|
||||
external: true
|
||||
name: ia1upsekrad96at5hq97e4qa
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redis_data:
|
||||
ollama_models:
|
||||
open_webui_data:
|
||||
neo4j_data:
|
||||
neo4j_logs:
|
||||
|
||||
|
||||
|
||||
plus_db:
|
||||
plus_storage:
|
||||
superset_home:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
# ─── Arcádia Suite — Produção (Coolify) ───────────────────────────────────────
|
||||
# Este arquivo é usado pelo Coolify para deploy automático.
|
||||
# NÃO inclui volumes de código-fonte — só artefatos de build.
|
||||
# Configurar no Coolify: Environment Variables para todas as vars ${...}
|
||||
|
||||
name: arcadia-prod
|
||||
|
||||
services:
|
||||
|
||||
# ── Banco de dados com pgvector ─────────────────────────────────────────────
|
||||
db:
|
||||
image: pgvector/pgvector:pg16
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: ${PGDATABASE:-arcadia}
|
||||
POSTGRES_USER: ${PGUSER:-arcadia}
|
||||
POSTGRES_PASSWORD: ${PGPASSWORD}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./docker/init-pgvector.sql:/docker-entrypoint-initdb.d/01-pgvector.sql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-arcadia}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── Redis ────────────────────────────────────────────────────────────────────
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: always
|
||||
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── App principal ─────────────────────────────────────────────────────────
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 5000
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
REDIS_URL: redis://redis:6379
|
||||
DOCKER_MODE: "true"
|
||||
CONTABIL_PYTHON_URL: http://contabil:8003
|
||||
BI_PYTHON_URL: http://bi:8004
|
||||
AUTOMATION_PYTHON_URL: http://automation:8005
|
||||
FISCO_PYTHON_URL: http://fisco:8002
|
||||
PYTHON_SERVICE_URL: http://embeddings:8001
|
||||
SESSION_SECRET: ${SESSION_SECRET}
|
||||
SSO_SECRET: ${SSO_SECRET}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
LITELLM_BASE_URL: http://litellm:4000
|
||||
LITELLM_API_KEY: ${LITELLM_API_KEY}
|
||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
|
||||
# ── Manus Agent — aponta para LiteLLM como gateway unificado ──────────
|
||||
# LiteLLM roteia para Ollama (local), LLMFit (fine-tuned) ou externo
|
||||
AI_INTEGRATIONS_OPENAI_BASE_URL: http://litellm:4000/v1
|
||||
AI_INTEGRATIONS_OPENAI_API_KEY: ${LITELLM_API_KEY}
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
networks:
|
||||
- arcadia-internal
|
||||
- arcadia-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.arcadia.rule=Host(`${DOMAIN}`)"
|
||||
- "traefik.http.routers.arcadia.tls=true"
|
||||
- "traefik.http.routers.arcadia.tls.certresolver=letsencrypt"
|
||||
|
||||
# ── Microserviços Python ─────────────────────────────────────────────────────
|
||||
contabil:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: always
|
||||
environment:
|
||||
SERVICE_NAME: contabil
|
||||
SERVICE_PORT: 8003
|
||||
CONTABIL_PORT: 8003
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
bi:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: always
|
||||
environment:
|
||||
SERVICE_NAME: bi
|
||||
SERVICE_PORT: 8004
|
||||
BI_PORT: 8004
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
automation:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: always
|
||||
environment:
|
||||
SERVICE_NAME: automation
|
||||
SERVICE_PORT: 8005
|
||||
AUTOMATION_PORT: 8005
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
fisco:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: always
|
||||
environment:
|
||||
SERVICE_NAME: fisco
|
||||
SERVICE_PORT: 8002
|
||||
FISCO_PORT: 8002
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
embeddings:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: always
|
||||
environment:
|
||||
SERVICE_NAME: embeddings
|
||||
SERVICE_PORT: 8001
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
|
||||
# Roteia: LLMFit (fine-tuned) → Ollama (local) → externo (opt-in)
|
||||
litellm:
|
||||
image: ghcr.io/berriai/litellm:main-latest
|
||||
restart: always
|
||||
volumes:
|
||||
- ./docker/litellm-config.yaml:/app/config.yaml
|
||||
command: ["--config", "/app/config.yaml", "--port", "4000"]
|
||||
environment:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
LITELLM_MASTER_KEY: ${LITELLM_API_KEY}
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
|
||||
# Ollama: se instalado no host use http://host-gateway:11434
|
||||
# Se usar container Docker, mantém http://ollama:11434
|
||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
|
||||
# LLMFit: URL do serviço de modelos fine-tuned
|
||||
LLMFIT_BASE_URL: ${LLMFIT_BASE_URL:-}
|
||||
# Providers externos opcionais (soberania: só habilitados se configurados)
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||
GROQ_API_KEY: ${GROQ_API_KEY:-}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- arcadia-internal
|
||||
|
||||
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
|
||||
# OPÇÃO A (padrão): Ollama como container Docker
|
||||
# OPÇÃO B: Ollama no host → comente este serviço e defina
|
||||
# OLLAMA_BASE_URL=http://host-gateway:11434 nas env vars
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- ollama_models:/root/.ollama
|
||||
networks:
|
||||
- arcadia-internal
|
||||
# Remova 'profiles: [ai]' para ativar por padrão no deploy
|
||||
profiles: [ai]
|
||||
|
||||
# ── Open WebUI (interface para Ollama + LLMFit) ───────────────────────────────
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
restart: always
|
||||
environment:
|
||||
# Pode apontar para LiteLLM para ter acesso a todos os modelos via WebUI
|
||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
|
||||
OPENAI_API_BASE_URL: http://litellm:4000/v1
|
||||
OPENAI_API_KEY: ${LITELLM_API_KEY}
|
||||
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY}
|
||||
volumes:
|
||||
- open_webui_data:/app/backend/data
|
||||
depends_on:
|
||||
- litellm
|
||||
networks:
|
||||
- arcadia-internal
|
||||
- arcadia-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.webui.rule=Host(`ai.${DOMAIN}`)"
|
||||
- "traefik.http.routers.webui.tls=true"
|
||||
- "traefik.http.routers.webui.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.webui.loadbalancer.server.port=8080"
|
||||
profiles: [ai]
|
||||
|
||||
networks:
|
||||
arcadia-internal:
|
||||
driver: bridge
|
||||
arcadia-public:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redis_data:
|
||||
ollama_models:
|
||||
open_webui_data:
|
||||
|
|
@ -1,87 +1,340 @@
|
|||
version: "3.8"
|
||||
# ─── Arcádia Suite — Desenvolvimento Local ────────────────────────────────────
|
||||
# Uso: docker compose up
|
||||
# Para subir com IA local: docker compose --profile ai up
|
||||
|
||||
name: arcadia-dev
|
||||
|
||||
services:
|
||||
|
||||
# ── Banco de dados com pgvector ─────────────────────────────────────────────
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
restart: always
|
||||
image: pgvector/pgvector:pg16
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${PGUSER:-arcadia}
|
||||
POSTGRES_PASSWORD: ${PGPASSWORD:-SuaSenhaSegura}
|
||||
POSTGRES_DB: ${PGDATABASE:-arcadia}
|
||||
POSTGRES_DB: arcadia
|
||||
POSTGRES_USER: arcadia
|
||||
POSTGRES_PASSWORD: arcadia123
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./docker/init-pgvector.sql:/docker-entrypoint-initdb.d/01-pgvector.sql
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-arcadia}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U arcadia -d arcadia"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ── Redis (filas de jobs) ────────────────────────────────────────────────────
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ── App principal (Node.js + React) ─────────────────────────────────────────
|
||||
app:
|
||||
build: .
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: builder # usa stage builder em dev (hot reload via volume)
|
||||
restart: unless-stopped
|
||||
command: npx tsx server/index.ts
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
PORT: 5000
|
||||
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
|
||||
REDIS_URL: redis://redis:6379
|
||||
DOCKER_MODE: "true" # desativa spawn de processos filhos
|
||||
CONTABIL_PYTHON_URL: http://contabil:8003
|
||||
BI_PYTHON_URL: http://bi:8004
|
||||
AUTOMATION_PYTHON_URL: http://automation:8005
|
||||
FISCO_PYTHON_URL: http://fisco:8002
|
||||
PYTHON_SERVICE_URL: http://embeddings:8001
|
||||
SESSION_SECRET: ${SESSION_SECRET:-arcadia-dev-secret-change-in-prod}
|
||||
SSO_SECRET: ${SSO_SECRET:-arcadia-sso-secret-2024-plus-integration-key-secure}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
LITELLM_BASE_URL: http://litellm:4000
|
||||
LITELLM_API_KEY: ${LITELLM_API_KEY:-arcadia-internal}
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /app/dist
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "5000:5000"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/${PGDATABASE:-arcadia}
|
||||
PGHOST: db
|
||||
NODE_ENV: production
|
||||
volumes:
|
||||
- uploads:/app/uploads
|
||||
redis:
|
||||
condition: service_healthy
|
||||
contabil:
|
||||
condition: service_started
|
||||
bi:
|
||||
condition: service_started
|
||||
|
||||
# ── Superset BI ──────────────────────────────────────────────────────────────
|
||||
superset:
|
||||
# ── Microserviço Contábil (Python) ──────────────────────────────────────────
|
||||
contabil:
|
||||
build:
|
||||
context: ./docker/superset
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-dev}
|
||||
SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/superset
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/superset
|
||||
PYTHONPATH: /app/pythonpath
|
||||
SERVICE_NAME: contabil
|
||||
SERVICE_PORT: 8003
|
||||
CONTABIL_PORT: 8003
|
||||
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
|
||||
ports:
|
||||
- "8003:8003"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# ── Microserviço BI Engine (Python) ─────────────────────────────────────────
|
||||
bi:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SERVICE_NAME: bi
|
||||
SERVICE_PORT: 8004
|
||||
BI_PORT: 8004
|
||||
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
|
||||
ports:
|
||||
- "8004:8004"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# ── Microserviço Automações (Python) ────────────────────────────────────────
|
||||
automation:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SERVICE_NAME: automation
|
||||
SERVICE_PORT: 8005
|
||||
AUTOMATION_PORT: 8005
|
||||
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
|
||||
ports:
|
||||
- "8005:8005"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# ── Microserviço Fiscal (Python) ────────────────────────────────────────────
|
||||
fisco:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SERVICE_NAME: fisco
|
||||
SERVICE_PORT: 8002
|
||||
FISCO_PORT: 8002
|
||||
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
|
||||
ports:
|
||||
- "8002:8002"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# ── Serviço de Embeddings (pgvector via Python) ──────────────────────────────
|
||||
embeddings:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.python
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SERVICE_NAME: embeddings
|
||||
SERVICE_PORT: 8001
|
||||
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
|
||||
ports:
|
||||
- "8001:8001"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# ── Apache Superset (BI avançado) ────────────────────────────────────────────
|
||||
superset:
|
||||
image: apache/superset:4.1.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
|
||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia_superset
|
||||
ARCADIA_DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia
|
||||
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
|
||||
SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USER:-admin}
|
||||
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:-arcadia2026}
|
||||
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
|
||||
ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/${PGDATABASE:-arcadia}
|
||||
command: ["/bin/bash", "/app/docker/init.sh"]
|
||||
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:-arcadia2026}
|
||||
SUPERSET_WEBSERVER_PORT: 8088
|
||||
PYTHONPATH: /app/pythonpath
|
||||
FLASK_ENV: production
|
||||
ports:
|
||||
- "8088:8088"
|
||||
volumes:
|
||||
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
|
||||
- ./docker/superset/init.sh:/app/docker/init.sh:ro
|
||||
- superset_home:/app/superset_home
|
||||
ports:
|
||||
- "8088:8088"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
command: ["/bin/bash", "/app/docker/init.sh"]
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
networks:
|
||||
- arcadia
|
||||
profiles: [bi]
|
||||
|
||||
# ── Neo4j (Knowledge Graph) ─────────────────────────────────────────────────
|
||||
# Ativar com: docker compose --profile kg up
|
||||
neo4j:
|
||||
image: neo4j:5
|
||||
# ─────────────── PERFIL: ai (Soberania de IA) ────────────────────────────────
|
||||
|
||||
# ── Ollama (LLMs locais) ─────────────────────────────────────────────────────
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ollama_models:/root/.ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
profiles: [ai]
|
||||
# Para GPU NVIDIA: adicione `deploy.resources.reservations.devices`
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: 1
|
||||
# capabilities: [gpu]
|
||||
|
||||
# ── Open WebUI (interface para devs/consultores) ─────────────────────────────
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
OLLAMA_BASE_URL: http://ollama:11434
|
||||
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:-webui-secret-change-in-prod}
|
||||
DEFAULT_MODELS: llama3.3,qwen2.5-coder
|
||||
ENABLE_RAG_WEB_SEARCH: "true"
|
||||
ports:
|
||||
- "3001:8080"
|
||||
volumes:
|
||||
- open_webui_data:/app/backend/data
|
||||
depends_on:
|
||||
- ollama
|
||||
profiles: [ai]
|
||||
|
||||
# ── LiteLLM (proxy unificado de LLMs) ───────────────────────────────────────
|
||||
litellm:
|
||||
image: ghcr.io/berriai/litellm:main-latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./docker/litellm-config.yaml:/app/config.yaml
|
||||
command: ["--config", "/app/config.yaml", "--port", "4000", "--detailed_debug"]
|
||||
environment:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
OLLAMA_BASE_URL: http://ollama:11434
|
||||
LITELLM_MASTER_KEY: ${LITELLM_API_KEY:-arcadia-internal}
|
||||
ports:
|
||||
- "4000:4000"
|
||||
profiles: [ai]
|
||||
|
||||
# ─────────────── PERFIL: erpnext (ERPNext local) ──────────────────────────────
|
||||
|
||||
# ── MariaDB para ERPNext ─────────────────────────────────────────────────────
|
||||
erpnext-db:
|
||||
image: mariadb:10.6
|
||||
restart: unless-stopped
|
||||
profiles: [erpnext]
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${ERPNEXT_DB_ROOT_PASSWORD:-erpnext-root-2026}
|
||||
MYSQL_DATABASE: _frappe
|
||||
MYSQL_USER: frappe
|
||||
MYSQL_PASSWORD: ${ERPNEXT_DB_PASSWORD:-frappe2026}
|
||||
volumes:
|
||||
- erpnext_db:/var/lib/mysql
|
||||
command: >
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_unicode_ci
|
||||
--skip-character-set-client-handshake
|
||||
--skip-innodb-read-only-compressed
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=${ERPNEXT_DB_ROOT_PASSWORD:-erpnext-root-2026}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
# ── Neo4j (Knowledge Graph) ──────────────────────────────────────────────────
|
||||
neo4j:
|
||||
image: neo4j:5.20-community
|
||||
restart: unless-stopped
|
||||
profiles: [agentic]
|
||||
environment:
|
||||
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-arcadia123}
|
||||
NEO4J_PLUGINS: '["apoc"]'
|
||||
NEO4J_dbms_security_procedures_unrestricted: apoc.*
|
||||
volumes:
|
||||
- neo4j_data:/data
|
||||
- neo4j_logs:/logs
|
||||
ports:
|
||||
- "7474:7474"
|
||||
- "7687:7687"
|
||||
profiles: [kg]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:7474 || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- arcadia
|
||||
|
||||
# ── ERPNext (Frappe Framework + ERPNext app) ─────────────────────────────────
|
||||
erpnext:
|
||||
image: frappe/erpnext:version-15
|
||||
restart: unless-stopped
|
||||
profiles: [erpnext]
|
||||
environment:
|
||||
FRAPPE_SITE_NAME_HEADER: erpnext.local
|
||||
BACKEND_PORT: 8000
|
||||
volumes:
|
||||
- erpnext_sites:/home/frappe/frappe-bench/sites
|
||||
- erpnext_logs:/home/frappe/frappe-bench/logs
|
||||
- ./docker/erpnext/init.sh:/usr/local/bin/init-erpnext.sh:ro
|
||||
depends_on:
|
||||
erpnext-db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8090:8080"
|
||||
command: ["/bin/bash", "/usr/local/bin/init-erpnext.sh"]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:8080/api/method/frappe.ping || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 120s
|
||||
networks:
|
||||
- arcadia
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
uploads:
|
||||
ollama_models:
|
||||
open_webui_data:
|
||||
superset_home:
|
||||
erpnext_db:
|
||||
erpnext_sites:
|
||||
erpnext_logs:
|
||||
neo4j_data:
|
||||
neo4j_logs:
|
||||
|
||||
networks:
|
||||
arcadia:
|
||||
driver: bridge
|
||||
|
|
|
|||
|
|
@ -31,16 +31,6 @@ model_list:
|
|||
model: ollama/llama3.2:3b
|
||||
api_base: os.environ/OLLAMA_BASE_URL
|
||||
|
||||
- model_name: deepseek-r1
|
||||
litellm_params:
|
||||
model: ollama/deepseek-r1:14b
|
||||
api_base: os.environ/OLLAMA_BASE_URL
|
||||
|
||||
- model_name: arcadia-agent
|
||||
litellm_params:
|
||||
model: groq/llama-3.3-70b-versatile
|
||||
api_key: os.environ/GROQ_API_KEY
|
||||
|
||||
- model_name: nomic-embed-text
|
||||
litellm_params:
|
||||
model: ollama/nomic-embed-text
|
||||
|
|
@ -70,28 +60,31 @@ model_list:
|
|||
# api_key: os.environ/ANTHROPIC_API_KEY
|
||||
|
||||
# ── TIER 3: Groq (opt-in — inferência rápida sem dados persistidos) ──────────
|
||||
- model_name: groq-llama
|
||||
litellm_params:
|
||||
model: groq/llama-3.3-70b-versatile
|
||||
api_key: os.environ/GROQ_API_KEY
|
||||
# - model_name: groq-llama
|
||||
# litellm_params:
|
||||
# model: groq/llama-3.3-70b-versatile
|
||||
# api_key: os.environ/GROQ_API_KEY
|
||||
|
||||
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
|
||||
# Prioridade: DeepSeek R1 14B (TIER 2) → llama3.2 (fallback rápido)
|
||||
# Prioridade: LLMFit (TIER 1) → Ollama (TIER 2 — fallback)
|
||||
- model_name: arcadia-default
|
||||
litellm_params:
|
||||
model: ollama/deepseek-r1:14b
|
||||
api_base: os.environ/OLLAMA_BASE_URL
|
||||
model: openai/llama3.2:3b
|
||||
api_base: os.environ/LLMFIT_BASE_URL
|
||||
api_key: os.environ/LLMFIT_API_KEY
|
||||
|
||||
router_settings:
|
||||
routing_strategy: least-busy
|
||||
fallbacks:
|
||||
- {"gpt-4o": ["arcadia-agent"]}
|
||||
- {"gpt-4o-mini": ["arcadia-agent"]}
|
||||
- {"gpt-4o": ["llama3.2"]}
|
||||
- {"gpt-4o-mini": ["llama3.2"]}
|
||||
- {"arcadia-default": ["llama3.2"]}
|
||||
- {"arcadia-finetuned": ["llama3.2"]}
|
||||
- {"arcadia-embed": ["nomic-embed-text"]}
|
||||
|
||||
litellm_settings:
|
||||
drop_params: true
|
||||
request_timeout: 300
|
||||
request_timeout: 120
|
||||
set_verbose: false
|
||||
|
||||
general_settings:
|
||||
|
|
|
|||
|
|
@ -10,36 +10,17 @@ ARCADIA_DB_URL="${ARCADIA_DATABASE_URL:-postgresql://arcadia:arcadia123@db:5432/
|
|||
|
||||
echo "[Superset Init] Aguardando PostgreSQL..."
|
||||
until python -c "
|
||||
import psycopg2, os, sys, re
|
||||
import psycopg2, os, sys
|
||||
try:
|
||||
# Health check no banco 'postgres' (sempre existe), não no 'superset' que ainda não existe
|
||||
url = os.environ.get('DATABASE_URL', 'postgresql://arcadia:SuaSenhaSegura@db:5432/superset')
|
||||
check_url = re.sub(r'/[^/?]+(\?.*)?$', '/postgres', url)
|
||||
psycopg2.connect(check_url)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
url = os.environ.get('DATABASE_URL', 'postgresql://arcadia:arcadia123@db:5432/arcadia_superset')
|
||||
psycopg2.connect(url)
|
||||
sys.exit(0)
|
||||
except: sys.exit(1)
|
||||
" 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
echo "[Superset Init] PostgreSQL disponível!"
|
||||
|
||||
echo "[Superset Init] Criando banco superset se necessário..."
|
||||
python - <<'DBEOF'
|
||||
import psycopg2, os, re
|
||||
url = os.environ.get('DATABASE_URL', 'postgresql://arcadia:SuaSenhaSegura@db:5432/superset')
|
||||
conn_url = re.sub(r'/[^/?]+(\?.*)?$', '/postgres', url)
|
||||
conn = psycopg2.connect(conn_url)
|
||||
conn.autocommit = True
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT 1 FROM pg_database WHERE datname = 'superset'")
|
||||
if not cur.fetchone():
|
||||
cur.execute("CREATE DATABASE superset")
|
||||
print("[Superset Init] Banco 'superset' criado.")
|
||||
else:
|
||||
print("[Superset Init] Banco 'superset' já existe.")
|
||||
conn.close()
|
||||
DBEOF
|
||||
|
||||
echo "[Superset Init] Rodando migrações do banco..."
|
||||
superset db upgrade
|
||||
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ SQLLAB_ASYNC_TIME_LIMIT_SEC = 300
|
|||
|
||||
# ── Branding Arcádia ──────────────────────────────────────────────────────────
|
||||
APP_NAME = "Arcádia Insights"
|
||||
APP_ICON = "/static/assets/images/arcadia_logo.png"
|
||||
APP_ICON_WIDTH = 150
|
||||
LOGO_TARGET_PATH = "/"
|
||||
FAVICONS = [{"href": "/static/assets/images/favicon.png"}]
|
||||
|
||||
|
|
@ -57,42 +55,8 @@ SESSION_COOKIE_SECURE = False # True em prod com HTTPS
|
|||
WTF_CSRF_ENABLED = True
|
||||
WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log"]
|
||||
|
||||
# ── Embedding cross-origin ────────────────────────────────────────────────────
|
||||
# Desabilita Flask-Talisman para permitir embedding via iframe cross-origin
|
||||
# (HTTPS é gerenciado pelo Traefik upstream)
|
||||
TALISMAN_ENABLED = False
|
||||
HTTP_HEADERS = {}
|
||||
X_FRAME_OPTIONS = "ALLOWALL"
|
||||
|
||||
# ── Guest Token (para embedding) ──────────────────────────────────────────────
|
||||
GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutos — frontend renova automaticamente
|
||||
GUEST_ROLE_NAME = "Public"
|
||||
GUEST_TOKEN_JWT_ALGO = "HS256"
|
||||
GUEST_TOKEN_HEADER_NAME = "X-GuestToken"
|
||||
|
||||
# ── Fix: g.user a partir do Bearer JWT ────────────────────────────────────────
|
||||
# Quando o papel Public tem can_read on Dashboard, o decorator protect() do FAB
|
||||
# trata o endpoint como público e pula a verificação JWT, deixando g.user como
|
||||
# AnonymousUser. Este hook corrige isso: se há um Bearer JWT válido e g.user é
|
||||
# anônimo, carrega o usuário do JWT antes do handler ser invocado.
|
||||
def FLASK_APP_MUTATOR(app): # noqa
|
||||
@app.before_request
|
||||
def _set_g_user_from_bearer_jwt():
|
||||
from flask import g, request # noqa
|
||||
user = getattr(g, "user", None)
|
||||
if user is not None and not getattr(user, "is_anonymous", True):
|
||||
return # já autenticado (ex: guest token via request_loader)
|
||||
auth = request.headers.get("Authorization", "")
|
||||
if not auth.startswith("Bearer "):
|
||||
return
|
||||
try:
|
||||
from flask_jwt_extended import decode_token # noqa
|
||||
decoded = decode_token(auth[7:])
|
||||
user_id = decoded.get("sub")
|
||||
if user_id is not None:
|
||||
from superset.extensions import security_manager as sm # noqa
|
||||
jwt_user = sm.get_user_by_id(int(user_id))
|
||||
if jwt_user and jwt_user.is_active:
|
||||
g.user = jwt_user
|
||||
except Exception: # noqa
|
||||
pass
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,63 @@
|
|||
-- Migration 0003: Agentic Suite — Skills (POO) + Skill Executions
|
||||
-- Fase 1 do planejamento estratégico Arcádia Agentic Suite
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "skills" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"tenant_id" INTEGER,
|
||||
"company_id" INTEGER,
|
||||
"user_id" VARCHAR,
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"slug" VARCHAR(255) NOT NULL,
|
||||
"namespace" VARCHAR(20) NOT NULL DEFAULT 'tenant',
|
||||
"description" TEXT,
|
||||
"version" VARCHAR(20) NOT NULL DEFAULT '1.0.0',
|
||||
"tags" TEXT[] DEFAULT '{}',
|
||||
"extends" TEXT[] DEFAULT '{}',
|
||||
"implements" TEXT[] DEFAULT '{}',
|
||||
"dependencies" JSONB DEFAULT '[]',
|
||||
"execute_visibility" VARCHAR(20) DEFAULT 'public',
|
||||
"parameters_visibility" VARCHAR(20) DEFAULT 'public',
|
||||
"trigger_type" VARCHAR(30),
|
||||
"trigger_config" JSONB,
|
||||
"parameters_schema" JSONB DEFAULT '{}',
|
||||
"return_schema" JSONB DEFAULT '{}',
|
||||
"body" TEXT,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
"is_system" BOOLEAN DEFAULT false,
|
||||
"created_by" VARCHAR,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Índice único: mesmo slug não pode repetir dentro do mesmo tenant
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "skills_tenant_slug_idx"
|
||||
ON "skills"("tenant_id", "slug")
|
||||
WHERE "tenant_id" IS NOT NULL;
|
||||
|
||||
-- Skills de sistema (sem tenant) também precisam slug único
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "skills_system_slug_idx"
|
||||
ON "skills"("slug")
|
||||
WHERE "tenant_id" IS NULL AND "is_system" = true;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "skill_executions" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"skill_id" UUID NOT NULL REFERENCES "skills"("id") ON DELETE CASCADE,
|
||||
"tenant_id" INTEGER,
|
||||
"triggered_by" VARCHAR(30) NOT NULL DEFAULT 'manual',
|
||||
"triggered_by_user_id" VARCHAR,
|
||||
"triggered_by_agent_id" VARCHAR(100),
|
||||
"parameters" JSONB DEFAULT '{}',
|
||||
"result" JSONB,
|
||||
"error" TEXT,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
"duration_ms" INTEGER,
|
||||
"steps_count" INTEGER DEFAULT 0,
|
||||
"correlation_id" UUID DEFAULT gen_random_uuid(),
|
||||
"audit_hash" VARCHAR(64),
|
||||
"started_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completed_at" TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "skill_executions_skill_id_idx" ON "skill_executions"("skill_id");
|
||||
CREATE INDEX IF NOT EXISTS "skill_executions_tenant_id_idx" ON "skill_executions"("tenant_id");
|
||||
CREATE INDEX IF NOT EXISTS "skill_executions_started_at_idx" ON "skill_executions"("started_at" DESC);
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
-- Migration 0004: OpenClaw — Detecção de Padrões e Sugestões de Skills Emergentes
|
||||
-- Fase 4 do planejamento estratégico Arcádia Agentic Suite
|
||||
|
||||
-- Tabela de padrões detectados
|
||||
CREATE TABLE IF NOT EXISTS "detected_patterns" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"tenant_id" INTEGER REFERENCES "tenants"("id"),
|
||||
"user_id" VARCHAR REFERENCES "users"("id"),
|
||||
|
||||
-- Padrão detectado
|
||||
"action_type" VARCHAR(100) NOT NULL,
|
||||
"description" TEXT,
|
||||
"frequency" INTEGER NOT NULL DEFAULT 0,
|
||||
"confidence" NUMERIC(4, 3) NOT NULL DEFAULT '0',
|
||||
|
||||
-- Janela de análise
|
||||
"first_seen_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_seen_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Metadados do padrão (contexto, módulos envolvidos, etc.)
|
||||
"metadata" JSONB DEFAULT '{}',
|
||||
|
||||
-- Ciclo de vida: active | archived | converted
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "detected_patterns_tenant_user_idx"
|
||||
ON "detected_patterns"("tenant_id", "user_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "detected_patterns_action_type_idx"
|
||||
ON "detected_patterns"("action_type");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "detected_patterns_status_idx"
|
||||
ON "detected_patterns"("status");
|
||||
|
||||
-- Tabela de sugestões de skills emergentes
|
||||
CREATE TABLE IF NOT EXISTS "skill_suggestions" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"pattern_id" UUID REFERENCES "detected_patterns"("id") ON DELETE CASCADE,
|
||||
"tenant_id" INTEGER REFERENCES "tenants"("id"),
|
||||
"user_id" VARCHAR REFERENCES "users"("id"),
|
||||
|
||||
-- Skill sugerida
|
||||
"suggested_skill_name" VARCHAR(200) NOT NULL,
|
||||
"suggested_description" TEXT,
|
||||
"estimated_automation" TEXT,
|
||||
"confidence" NUMERIC(4, 3) NOT NULL DEFAULT '0',
|
||||
|
||||
-- Skill gerada (após confirmação)
|
||||
"generated_skill_id" UUID REFERENCES "skills"("id"),
|
||||
|
||||
-- Estado da sugestão: pending | accepted | rejected | expired
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
|
||||
-- Rastreabilidade
|
||||
"source" VARCHAR(50) NOT NULL DEFAULT 'openclaw',
|
||||
"reviewed_by" VARCHAR REFERENCES "users"("id"),
|
||||
"reviewed_at" TIMESTAMP,
|
||||
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "skill_suggestions_pattern_id_idx"
|
||||
ON "skill_suggestions"("pattern_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "skill_suggestions_tenant_user_idx"
|
||||
ON "skill_suggestions"("tenant_id", "user_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "skill_suggestions_status_idx"
|
||||
ON "skill_suggestions"("status");
|
||||
|
|
@ -8,6 +8,20 @@
|
|||
"when": 1769121114659,
|
||||
"tag": "0000_low_tiger_shark",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1741824000000,
|
||||
"tag": "0001_whatsapp_auto_reply_config",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1741824060000,
|
||||
"tag": "0002_tenant_isolation",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -9,7 +9,10 @@
|
|||
"build": "tsx script/build.ts",
|
||||
"start": "NODE_ENV=production node dist/index.cjs",
|
||||
"check": "tsc",
|
||||
"db:push": "drizzle-kit push"
|
||||
"db:push": "drizzle-kit push",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:openclaw": "vitest run tests/openclaw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
|
|
@ -72,7 +75,6 @@
|
|||
"connect-pg-simple": "^10.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
fastapi==0.109.0
|
||||
uvicorn==0.27.0
|
||||
pandas==2.1.0
|
||||
numpy==1.26.0
|
||||
httpx==0.26.0
|
||||
python-multipart==0.0.6
|
||||
pydantic==2.5.0
|
||||
chromadb==0.4.22
|
||||
openai==1.12.0
|
||||
13
replit.md
13
replit.md
|
|
@ -46,27 +46,24 @@ The Arcádia Suite employs a 4-layer hybrid architecture:
|
|||
* **Arcádia Suite's 6-Layer Architecture:** Apps (Core, Business, Segment), Structural Modules (Financial, Accounting, Fiscal, HR), Operational Execution (Retail, Services, ERPs), Intelligence (Manus, Knowledge Graph, Automations), Platform (Central API, Hub API, MCP/A2A, IDE, Data Science), and Consulting (Process Compass, Production, Support).
|
||||
* **Unified Communication Center:** Integrates a WhatsApp Attendance System (multi-session, real-time sync, ticket management, AI auto-replies) and an Internal Chat System, feeding all communications into the Knowledge Graph.
|
||||
* **Learning System & Knowledge Graph:** The system learns from all interactions, storing Q&A and patterns. A semantic Knowledge Graph connects all business information via nodes and edges for insights.
|
||||
* **Manus Autonomous Agent (Cérebro Central):** Central AI brain (GPT-4o) powering all intelligence via ManusIntelligence singleton. All 6 Dev Center agents (Architect, Generator, Validator, Executor, Researcher, Evolution) route their AI thinking through ManusIntelligence, which provides context enrichment via ToolManager (code search, file reading). Executes multi-step tasks using a thought-action-observation loop with 56+ tools (web search, knowledge/ERP query, file I/O, shell commands, semantic search). Visible in Casa de Máquinas as "Motor IA" with health endpoint at `/api/manus/health` showing model, metrics (calls, tokens, errors), capabilities, and uptime.
|
||||
* **Manus Autonomous Agent:** Executes multi-step tasks using a thought-action-observation loop with various tools (web search, knowledge/ERP query, calculations, messaging, reporting, scheduling).
|
||||
* **Scientist Module:** An AI auto-programming component for data analysis, code generation (Python/SQL) in a sandbox, and solution storage.
|
||||
* **Arcádia Fisco (Fiscal Engine):** Centralized compliance motor for Brazilian tax regulations (NCMs, CFOPs, CESTs, tax groups, digital certificates), integrating with SEFAZ webservices for NF-e/NFC-e issuance.
|
||||
* **Arcádia SOE (Sistema Operacional Empresarial):** The central business operating system (kernel) accessible via `/soe`. Contains all canonical business domains (People, Products, Sales, Purchases, Inventory, Financial, Fiscal, CRM, Projects, Quality, Governance). Apps (Retail, Engineering, Food Service, etc.) extend SOE with segment-specific experiences. Motors (Plus, ERPNext, Fisco, BI) are execution engines selected via server-side adapter pattern. API at `/api/soe/*` (backward-compatible alias from `/api/erp/*`). Context: `SoeMotorContext` replaces `ErpProfileContext`. Legacy file `ERP.tsx` preserved for reference.
|
||||
* **Arcádia ERP (Gestão Empresarial):** A complete ERP module integrating ERPNext with Arcádia's intelligence, accessible via `/erp`. Includes dashboards, unified people module, inventory, sales, purchases, financial management, AI-driven reports, and integration with Arcádia Fisco and Manus AI.
|
||||
* **Arcádia Plus (ERP Laravel):** A standalone Laravel/PHP ERP, accessible via `/plus`, with extensive features including NF-e/NFC-e/CT-e/MDF-e, POS, digital menu, service orders, financial management, stock traceability, CRM, and integrations with e-commerce platforms. Uses an isolated PostgreSQL schema.
|
||||
* **Business Intelligence (Arcádia Insights):** Provides data visualization and analysis, powered by the BI Engine (FastAPI, port 8004) with SQL query execution, chart-data generation, micro-BI API, caching layer, and Pandas analysis.
|
||||
* **Automation Engine:** FastAPI service (port 8005) providing cron scheduler, event bus, and workflow executor for the Automations module.
|
||||
* **Communication Engine (Motor de Comunicação):** Node/TypeScript service (port 8006) unifying XOS CRM, Arcádia CRM, WhatsApp, and Email into a single managed engine. Provides unified API for contacts, threads, messages, channels, queues, quick messages, stats, and agent context (360° view). Emits events to `comm_events` table consumed by Knowledge Graph and AI agents. Proxy at `/api/comm/*`. Canonical tables: `comm_contacts`, `comm_threads`, `comm_messages`, `comm_channels`, `comm_queues`, `comm_quick_messages`, `comm_events`. Backward-compatible via reference fields (`xos_contact_id`, `crm_client_id`, etc.).
|
||||
* **Casa de Máquinas (Engine Room):** Unified control panel at `/engine-room` showing real-time status of all engines (Manus IA, Plus 8080, Contábil 8003, Fiscal 8002, BI 8004, Automation 8005, Communication 8006) and XOS agents. Manus IA has a dedicated tab with metrics (model, calls, tokens, errors, uptime, capabilities). API at `/api/engine-room/*`.
|
||||
* **Casa de Máquinas (Engine Room):** Unified control panel at `/engine-room` showing real-time status of all engines (Plus 8080, Contábil 8003, Fiscal 8002, BI 8004, Automation 8005, Communication 8006) and XOS agents. API at `/api/engine-room/*`.
|
||||
* **IDE:** Integrated development environment with Monaco Editor and Xterm.js.
|
||||
* **Module Activation System:** Per-tenant module management via `tenants.features` JSONB column (TenantFeatures type). API at `GET/PUT /api/soe/tenant/modules`. Admin → Módulos tab provides toggle switches for each module. Backend gating middleware (`requireModule`) blocks sync endpoints when modules are disabled (returns 403 with actionable message). SOE Sync Dashboard at `/soe` tab "Sincronização" shows motor status, enabled/disabled state, and manual sync buttons. Frontend hook `useTenantFeatures()` provides feature checks. Feature keys: ide, whatsapp, crm, erp, bi, manus, retail, plus, fisco, cockpit, compass, production, support, xosCrm, centralApis, comunidades, biblioteca.
|
||||
* **Multi-Tenant Architecture:** Supports a hierarchical structure for Master, Partners, and Clients.
|
||||
* **Agentic Interoperability Protocols:** Implements MCP (Model Context Protocol) for tool exposure (`/api/mcp/v1/`), A2A (Agent to Agent Protocol) for bidirectional communication (`/api/a2a/v1/`), and planned AP2 (Agent Payment Protocol) and UCP (Unified Commerce Protocol). Authentication uses `X-API-Key`.
|
||||
* **Autonomous Development:** Integrated with GitHub for automatic commits, branch creation, pull requests, and repository analysis using a `ToolManager` system.
|
||||
* **XOS Governance Layer:** Policy evaluation engine with fail-closed security, immutable audit trail, tool registry (auto-synced from ToolManager), skill registry with usage tracking, and contract registry. Integrated into BaseBlackboardAgent and ToolManager for automatic policy checks. API at `/api/governance/*`. 5 security policies: critical file protection, read permissions, destructive command blocking, human approval for production deploys, automatic staging with validation score threshold. Phase 3 adds PostgreSQL Job Queue (xos_job_queue table with priority, retry, dead-letter), Agent Metrics (xos_agent_metrics), 6th Researcher agent, and a visual Governance Dashboard at `/xos/governance` with real-time stats, audit trail, policies, tools, skills, jobs, and agent monitoring. Phase 4 adds Autonomous Pipeline Orchestrator (PipelineOrchestrator.ts) that chains all 6 agents: Portuguese prompt → Architect (design) → Generator (codegen) → Validator (typecheck) → Executor (staging) → Evolution (learn). Staging review system (xos_staging_changes + xos_dev_pipelines tables) with explicit user approval before applying code. SSE streaming for real-time progress. Pipeline UI integrated as "Pipeline" tab within Dev Center (`/dev-center`) with enhanced prompt input (image paste, file attachment, plan section), live timeline, staged code review/approve/reject. `/xos/pipeline` redirects to Dev Center. API at `/api/xos/pipeline`.
|
||||
|
||||
* **Modular Dev Center Architecture:** The XOS Pipeline agents can autonomously create complete modules (database schemas, API routes, and UI pages) using a modular system. Module schemas go to `shared/schemas/{moduleName}.ts` and are auto-registered in the main schema via `shared/schemas/index.ts`. Module routes go to `server/modules/{moduleName}.ts` and are auto-loaded at startup via `server/modules/loader.ts`, mounted at `/api/modules/{moduleName}`. After pipeline approval, the migrator (`server/modules/migrator.ts`) auto-registers schemas and runs `drizzle-kit push`. Core files (`shared/schema.ts`, `server/routes.ts`, `db/index.ts`, `shared/schemas/index.ts`, `shared/schemas/loader.ts`, `server/modules/loader.ts`) remain protected. Agent prompts (Architect, Generator) include instructions for the modular pattern. API at `/api/xos/pipeline/modules/status` shows registered schemas and loaded routes.
|
||||
* **XOS Governance Layer:** Policy evaluation engine with fail-closed security, immutable audit trail, tool registry (auto-synced from ToolManager), skill registry with usage tracking, and contract registry. Integrated into BaseBlackboardAgent and ToolManager for automatic policy checks. API at `/api/governance/*`. 5 security policies: critical file protection, read permissions, destructive command blocking, human approval for production deploys, automatic staging with validation score threshold. Phase 3 adds PostgreSQL Job Queue (xos_job_queue table with priority, retry, dead-letter), Agent Metrics (xos_agent_metrics), 6th Researcher agent, and a visual Governance Dashboard at `/xos/governance` with real-time stats, audit trail, policies, tools, skills, jobs, and agent monitoring. Phase 4 adds Autonomous Pipeline Orchestrator (PipelineOrchestrator.ts) that chains all 6 agents: Portuguese prompt → Architect (design) → Generator (codegen) → Validator (typecheck) → Executor (staging) → Evolution (learn). Staging review system (xos_staging_changes + xos_dev_pipelines tables) with explicit user approval before applying code. SSE streaming for real-time progress. Pipeline UI at `/xos/pipeline` with prompt submission, live timeline, and staged code review/approve/reject. API at `/api/xos/pipeline`.
|
||||
|
||||
## External Dependencies
|
||||
|
||||
* **OpenAI API:** For AI capabilities via ManusIntelligence (GPT-4o for Dev Center agents) and auto-replies (gpt-4o-mini for WhatsApp).
|
||||
* **OpenAI API:** For AI capabilities, including auto-replies (`gpt-4o-mini`) and the Scientist Module.
|
||||
* **Baileys Library:** For WhatsApp integration and multi-session management.
|
||||
* **nfelib (Python):** For Brazilian fiscal document (NF-e/NFC-e) issuance, validation, and SEFAZ communication.
|
||||
* **PostgreSQL:** Primary database, managed with Drizzle ORM.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Router, Request, Response } from "express";
|
||||
import { db } from "../../db/index";
|
||||
import { eq, desc, and, or, isNull, inArray, sql } from "drizzle-orm";
|
||||
import { users, profiles, crmPartners, crmClients, tenants, tenantPlans, partnerClients, partnerCommissions, tenantEmpresas, insertTenantSchema, insertTenantPlanSchema, insertPartnerClientSchema, insertTenantEmpresaSchema } from "@shared/schema";
|
||||
import { users, profiles, crmPartners, crmClients, tenants, tenantPlans, partnerClients, partnerCommissions, insertTenantSchema, insertTenantPlanSchema, insertPartnerClientSchema } from "@shared/schema";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ router.use((req: Request, res: Response, next) => {
|
|||
return res.status(401).json({ error: "Não autorizado" });
|
||||
}
|
||||
const user = req.user as any;
|
||||
if (user.role !== "admin" && user.role !== "master") {
|
||||
if (user.role !== "admin") {
|
||||
return res.status(403).json({ error: "Acesso negado. Apenas administradores." });
|
||||
}
|
||||
next();
|
||||
|
|
@ -317,25 +317,10 @@ router.get("/tenants", async (req: Request, res: Response) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Helper: resolve plan features for a tenant based on plan code
|
||||
async function resolvePlanFeatures(planCode: string): Promise<Record<string, any> | null> {
|
||||
const [plan] = await db.select().from(tenantPlans).where(eq(tenantPlans.code, planCode));
|
||||
if (!plan || !plan.features) return null;
|
||||
return plan.features as Record<string, any>;
|
||||
}
|
||||
|
||||
// POST /api/admin/tenants - Create new tenant
|
||||
router.post("/tenants", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const validated = insertTenantSchema.parse(req.body);
|
||||
|
||||
if (validated.plan && !validated.features) {
|
||||
const planFeatures = await resolvePlanFeatures(validated.plan);
|
||||
if (planFeatures) {
|
||||
validated.features = planFeatures as any;
|
||||
}
|
||||
}
|
||||
|
||||
const [tenant] = await db.insert(tenants).values(validated).returning();
|
||||
res.status(201).json(tenant);
|
||||
} catch (error: any) {
|
||||
|
|
@ -349,41 +334,19 @@ router.patch("/tenants/:id", async (req: Request, res: Response) => {
|
|||
const id = parseInt(req.params.id);
|
||||
const user = req.user as any;
|
||||
|
||||
// Verificar permissão de acesso
|
||||
if (!await canAccessTenant(user, id)) {
|
||||
return res.status(403).json({ error: "Sem permissão para modificar este tenant" });
|
||||
}
|
||||
|
||||
const { name, email, phone, plan, status, tenantType, parentTenantId, partnerCode, commissionRate, maxUsers, maxStorageMb, features, billingEmail, trialEndsAt } = req.body;
|
||||
|
||||
let resolvedFeatures = features;
|
||||
if (plan && !features) {
|
||||
const [currentTenant] = await db.select().from(tenants).where(eq(tenants.id, id));
|
||||
if (currentTenant && currentTenant.plan !== plan) {
|
||||
const planFeatures = await resolvePlanFeatures(plan);
|
||||
if (planFeatures) {
|
||||
resolvedFeatures = planFeatures;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: any = { updatedAt: new Date() };
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (email !== undefined) updateData.email = email;
|
||||
if (phone !== undefined) updateData.phone = phone;
|
||||
if (plan !== undefined) updateData.plan = plan;
|
||||
if (status !== undefined) updateData.status = status;
|
||||
if (tenantType !== undefined) updateData.tenantType = tenantType;
|
||||
if (parentTenantId !== undefined) updateData.parentTenantId = parentTenantId;
|
||||
if (partnerCode !== undefined) updateData.partnerCode = partnerCode;
|
||||
if (commissionRate !== undefined) updateData.commissionRate = commissionRate;
|
||||
if (maxUsers !== undefined) updateData.maxUsers = maxUsers;
|
||||
if (maxStorageMb !== undefined) updateData.maxStorageMb = maxStorageMb;
|
||||
if (resolvedFeatures !== undefined) updateData.features = resolvedFeatures;
|
||||
if (billingEmail !== undefined) updateData.billingEmail = billingEmail;
|
||||
if (trialEndsAt !== undefined) updateData.trialEndsAt = trialEndsAt;
|
||||
|
||||
const [tenant] = await db.update(tenants)
|
||||
.set(updateData)
|
||||
.set({
|
||||
name, email, phone, plan, status, tenantType, parentTenantId, partnerCode,
|
||||
commissionRate, maxUsers, maxStorageMb, features, billingEmail, trialEndsAt,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(tenants.id, id))
|
||||
.returning();
|
||||
|
||||
|
|
@ -504,121 +467,6 @@ router.delete("/plans/:id", async (req: Request, res: Response) => {
|
|||
}
|
||||
});
|
||||
|
||||
// POST /api/admin/plans/:id/propagate - Apply plan features to all tenants with this plan
|
||||
router.post("/plans/:id/propagate", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const [plan] = await db.select().from(tenantPlans).where(eq(tenantPlans.id, id));
|
||||
if (!plan) return res.status(404).json({ error: "Plano não encontrado" });
|
||||
if (!plan.features) return res.status(400).json({ error: "Plano sem módulos definidos" });
|
||||
|
||||
const affected = await db.update(tenants)
|
||||
.set({ features: plan.features, maxUsers: plan.maxUsers, maxStorageMb: plan.maxStorageMb, updatedAt: new Date() })
|
||||
.where(eq(tenants.plan, plan.code))
|
||||
.returning();
|
||||
|
||||
res.json({ propagated: affected.length, planCode: plan.code });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/admin/plans/seed - Seed default plans
|
||||
router.post("/plans/seed", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const existing = await db.select().from(tenantPlans);
|
||||
if (existing.length > 0) {
|
||||
return res.json({ message: "Planos já existem", count: existing.length });
|
||||
}
|
||||
|
||||
const defaultPlans: any[] = [
|
||||
{
|
||||
code: "free",
|
||||
name: "Gratuito",
|
||||
description: "Plano básico gratuito para experimentar",
|
||||
tenantType: "client",
|
||||
maxUsers: 2,
|
||||
maxStorageMb: 500,
|
||||
monthlyPrice: 0,
|
||||
yearlyPrice: 0,
|
||||
trialDays: 14,
|
||||
sortOrder: 1,
|
||||
features: { erp: true, crm: true },
|
||||
},
|
||||
{
|
||||
code: "starter",
|
||||
name: "Starter",
|
||||
description: "Para pequenas empresas começando",
|
||||
tenantType: "client",
|
||||
maxUsers: 5,
|
||||
maxStorageMb: 2000,
|
||||
monthlyPrice: 9900,
|
||||
yearlyPrice: 99000,
|
||||
trialDays: 14,
|
||||
sortOrder: 2,
|
||||
features: { erp: true, crm: true, bi: true, fisco: true, whatsapp: true, compass: true },
|
||||
},
|
||||
{
|
||||
code: "pro",
|
||||
name: "Profissional",
|
||||
description: "Para empresas em crescimento",
|
||||
tenantType: "client",
|
||||
maxUsers: 15,
|
||||
maxStorageMb: 10000,
|
||||
monthlyPrice: 29900,
|
||||
yearlyPrice: 299000,
|
||||
trialDays: 14,
|
||||
sortOrder: 3,
|
||||
features: { erp: true, crm: true, bi: true, fisco: true, retail: true, plus: true, whatsapp: true, manus: true, cockpit: true, compass: true, support: true, biblioteca: true },
|
||||
},
|
||||
{
|
||||
code: "enterprise",
|
||||
name: "Enterprise",
|
||||
description: "Para grandes empresas",
|
||||
tenantType: "client",
|
||||
maxUsers: 100,
|
||||
maxStorageMb: 50000,
|
||||
monthlyPrice: 99900,
|
||||
yearlyPrice: 999000,
|
||||
trialDays: 30,
|
||||
sortOrder: 4,
|
||||
features: { erp: true, crm: true, bi: true, fisco: true, retail: true, plus: true, whatsapp: true, manus: true, ide: true, cockpit: true, compass: true, production: true, support: true, xosCrm: true, centralApis: true, comunidades: true, biblioteca: true },
|
||||
},
|
||||
{
|
||||
code: "partner_starter",
|
||||
name: "Parceiro Starter",
|
||||
description: "Para consultores e pequenos integradores",
|
||||
tenantType: "partner",
|
||||
maxUsers: 10,
|
||||
maxStorageMb: 5000,
|
||||
monthlyPrice: 19900,
|
||||
yearlyPrice: 199000,
|
||||
trialDays: 14,
|
||||
sortOrder: 5,
|
||||
features: { erp: true, crm: true, bi: true, fisco: true, retail: true, whatsapp: true, compass: true, support: true },
|
||||
},
|
||||
{
|
||||
code: "partner_pro",
|
||||
name: "Parceiro Pro",
|
||||
description: "Para integradores e consultorias",
|
||||
tenantType: "partner",
|
||||
maxUsers: 50,
|
||||
maxStorageMb: 25000,
|
||||
monthlyPrice: 49900,
|
||||
yearlyPrice: 499000,
|
||||
trialDays: 30,
|
||||
sortOrder: 6,
|
||||
features: { erp: true, crm: true, bi: true, fisco: true, retail: true, plus: true, whatsapp: true, manus: true, ide: true, cockpit: true, compass: true, production: true, support: true, xosCrm: true, centralApis: true, comunidades: true, biblioteca: true },
|
||||
},
|
||||
];
|
||||
|
||||
const created = await db.insert(tenantPlans).values(defaultPlans).returning();
|
||||
res.status(201).json({ message: "Planos criados com sucesso", count: created.length, plans: created });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// PARTNER-CLIENT RELATIONSHIPS
|
||||
// ==========================================
|
||||
|
|
@ -770,106 +618,4 @@ router.get("/tenants/stats", async (req: Request, res: Response) => {
|
|||
}
|
||||
});
|
||||
|
||||
// ========== TENANT EMPRESAS (Matriz/Filiais) ==========
|
||||
|
||||
router.get("/empresas", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { tenantId } = req.query;
|
||||
const user = req.user as any;
|
||||
|
||||
let query;
|
||||
if (tenantId) {
|
||||
const tid = parseInt(tenantId as string);
|
||||
if (!(await canAccessTenant(user, tid))) {
|
||||
return res.status(403).json({ error: "Sem permissão para este tenant" });
|
||||
}
|
||||
query = await db.select().from(tenantEmpresas)
|
||||
.where(eq(tenantEmpresas.tenantId, tid))
|
||||
.orderBy(desc(tenantEmpresas.tipo), tenantEmpresas.razaoSocial);
|
||||
} else {
|
||||
const allowed = await getAllowedTenantIds(user);
|
||||
if (allowed) {
|
||||
query = await db.select().from(tenantEmpresas)
|
||||
.where(inArray(tenantEmpresas.tenantId, allowed))
|
||||
.orderBy(desc(tenantEmpresas.tipo), tenantEmpresas.razaoSocial);
|
||||
} else {
|
||||
query = await db.select().from(tenantEmpresas)
|
||||
.orderBy(desc(tenantEmpresas.tipo), tenantEmpresas.razaoSocial);
|
||||
}
|
||||
}
|
||||
res.json(query);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/empresas/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const [empresa] = await db.select().from(tenantEmpresas)
|
||||
.where(eq(tenantEmpresas.id, parseInt(req.params.id)));
|
||||
if (!empresa) return res.status(404).json({ error: "Empresa não encontrada" });
|
||||
|
||||
const user = req.user as any;
|
||||
if (!(await canAccessTenant(user, empresa.tenantId))) {
|
||||
return res.status(403).json({ error: "Sem permissão" });
|
||||
}
|
||||
res.json(empresa);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/empresas", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const data = insertTenantEmpresaSchema.parse(req.body);
|
||||
const user = req.user as any;
|
||||
if (!(await canAccessTenant(user, data.tenantId))) {
|
||||
return res.status(403).json({ error: "Sem permissão para este tenant" });
|
||||
}
|
||||
const [empresa] = await db.insert(tenantEmpresas).values(data).returning();
|
||||
res.json(empresa);
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.patch("/empresas/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const [existing] = await db.select().from(tenantEmpresas)
|
||||
.where(eq(tenantEmpresas.id, parseInt(req.params.id)));
|
||||
if (!existing) return res.status(404).json({ error: "Empresa não encontrada" });
|
||||
|
||||
const user = req.user as any;
|
||||
if (!(await canAccessTenant(user, existing.tenantId))) {
|
||||
return res.status(403).json({ error: "Sem permissão" });
|
||||
}
|
||||
|
||||
const [empresa] = await db.update(tenantEmpresas)
|
||||
.set({ ...req.body, updatedAt: new Date() })
|
||||
.where(eq(tenantEmpresas.id, parseInt(req.params.id)))
|
||||
.returning();
|
||||
res.json(empresa);
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/empresas/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const [existing] = await db.select().from(tenantEmpresas)
|
||||
.where(eq(tenantEmpresas.id, parseInt(req.params.id)));
|
||||
if (!existing) return res.status(404).json({ error: "Empresa não encontrada" });
|
||||
|
||||
const user = req.user as any;
|
||||
if (!(await canAccessTenant(user, existing.tenantId))) {
|
||||
return res.status(403).json({ error: "Sem permissão" });
|
||||
}
|
||||
|
||||
await db.delete(tenantEmpresas).where(eq(tenantEmpresas.id, parseInt(req.params.id)));
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -30,8 +30,12 @@ async function comparePasswords(supplied: string, stored: string) {
|
|||
return timingSafeEqual(hashedBuf, suppliedBuf);
|
||||
}
|
||||
|
||||
if (!process.env.SESSION_SECRET) {
|
||||
console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production.");
|
||||
}
|
||||
|
||||
const sessionSettings: session.SessionOptions = {
|
||||
secret: process.env.SESSION_SECRET || "arcadia-browser-secret-key-2024",
|
||||
secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: storage.sessionStore,
|
||||
|
|
|
|||
|
|
@ -4,109 +4,10 @@ import { db } from "../../../db/index";
|
|||
import { xosToolRegistry } from "@shared/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const WRITE_TOOLS = new Set([
|
||||
"write_file",
|
||||
"github_commit",
|
||||
"git_local_commit",
|
||||
"run_command",
|
||||
]);
|
||||
|
||||
const READ_ONLY_TOOLS = new Set([
|
||||
"read_file",
|
||||
"list_directory",
|
||||
"search_code",
|
||||
"git_status",
|
||||
"analyze_external_repo",
|
||||
"read_external_file",
|
||||
"typecheck",
|
||||
"metaset.query",
|
||||
"metaset.list_tables",
|
||||
"metaset.table_fields",
|
||||
"metaset.list_questions",
|
||||
"metaset.list_dashboards",
|
||||
"metaset.health",
|
||||
]);
|
||||
|
||||
interface PipelineContext {
|
||||
planMode: boolean;
|
||||
commandLog: Array<{ command: string; output: string; timestamp: string; success: boolean }>;
|
||||
}
|
||||
|
||||
export class ToolManager {
|
||||
private tools: Map<string, BaseTool> = new Map();
|
||||
private categories: Map<string, string[]> = new Map();
|
||||
private governanceSynced: boolean = false;
|
||||
private _activePipelineId: number | null = null;
|
||||
private _pipelineContexts: Map<number, PipelineContext> = new Map();
|
||||
private _globalCommandLog: Array<{ command: string; output: string; timestamp: string; success: boolean; pipelineId?: number }> = [];
|
||||
|
||||
private getPipelineContext(pipelineId?: number): PipelineContext | null {
|
||||
const id = pipelineId ?? this._activePipelineId;
|
||||
if (id === null) return null;
|
||||
if (!this._pipelineContexts.has(id)) {
|
||||
this._pipelineContexts.set(id, { planMode: false, commandLog: [] });
|
||||
}
|
||||
return this._pipelineContexts.get(id)!;
|
||||
}
|
||||
|
||||
setActivePipeline(pipelineId: number | null): void {
|
||||
this._activePipelineId = pipelineId;
|
||||
}
|
||||
|
||||
get activePipelineId(): number | null {
|
||||
return this._activePipelineId;
|
||||
}
|
||||
|
||||
get planMode(): boolean {
|
||||
const ctx = this.getPipelineContext();
|
||||
return ctx?.planMode ?? false;
|
||||
}
|
||||
|
||||
setPlanMode(enabled: boolean, pipelineId?: number): void {
|
||||
const id = pipelineId ?? this._activePipelineId;
|
||||
if (id !== null && id !== undefined) {
|
||||
const ctx = this.getPipelineContext(id);
|
||||
if (ctx) ctx.planMode = enabled;
|
||||
}
|
||||
console.log(`[ToolManager] Plan Mode pipeline=${id}: ${enabled ? "ATIVADO" : "DESATIVADO"}`);
|
||||
}
|
||||
|
||||
isWriteTool(toolName: string): boolean {
|
||||
return WRITE_TOOLS.has(toolName);
|
||||
}
|
||||
|
||||
getCommandLog(pipelineId?: number): typeof this._globalCommandLog {
|
||||
if (pipelineId !== undefined) {
|
||||
return this._globalCommandLog.filter(l => l.pipelineId === pipelineId);
|
||||
}
|
||||
return [...this._globalCommandLog];
|
||||
}
|
||||
|
||||
clearCommandLog(pipelineId?: number): void {
|
||||
if (pipelineId !== undefined) {
|
||||
this._globalCommandLog = this._globalCommandLog.filter(l => l.pipelineId !== pipelineId);
|
||||
} else {
|
||||
this._globalCommandLog = [];
|
||||
}
|
||||
}
|
||||
|
||||
addCommandLog(entry: { command: string; output: string; timestamp: string; success: boolean }): void {
|
||||
this._globalCommandLog.push({ ...entry, pipelineId: this._activePipelineId ?? undefined });
|
||||
if (this._globalCommandLog.length > 500) {
|
||||
this._globalCommandLog = this._globalCommandLog.slice(-250);
|
||||
}
|
||||
}
|
||||
|
||||
cleanupPipeline(pipelineId: number): void {
|
||||
this._pipelineContexts.delete(pipelineId);
|
||||
this._globalCommandLog = this._globalCommandLog.filter(l => l.pipelineId !== pipelineId);
|
||||
if (this._activePipelineId === pipelineId) this._activePipelineId = null;
|
||||
}
|
||||
|
||||
getToolsForMode(mode: "plan" | "act"): ToolDefinition[] {
|
||||
if (mode === "act") return this.listTools();
|
||||
return this.listTools().filter(t => !WRITE_TOOLS.has(t.name));
|
||||
}
|
||||
|
||||
register(tool: BaseTool): void {
|
||||
this.tools.set(tool.name, tool);
|
||||
|
|
@ -188,14 +89,6 @@ export class ToolManager {
|
|||
};
|
||||
}
|
||||
|
||||
if (this.planMode && WRITE_TOOLS.has(toolName)) {
|
||||
return {
|
||||
success: false,
|
||||
result: `[PLAN MODE] Ferramenta "${toolName}" bloqueada. Em modo planejamento, apenas ferramentas de leitura são permitidas. Analise o código e gere um plano detalhado.`,
|
||||
error: `PLAN_MODE_BLOCKED: ${toolName}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (agentName) {
|
||||
const rbacCheck = await this.checkRBAC(toolName, agentName);
|
||||
if (!rbacCheck.allowed) {
|
||||
|
|
@ -233,15 +126,6 @@ export class ToolManager {
|
|||
|
||||
console.log(`[ToolManager] ${toolName} executado em ${duration}ms`);
|
||||
|
||||
if (toolName === "run_command" || toolName === "typecheck") {
|
||||
this.addCommandLog({
|
||||
command: params.command || toolName,
|
||||
output: result.result?.slice(0, 5000) || "",
|
||||
timestamp: new Date().toISOString(),
|
||||
success: result.success,
|
||||
});
|
||||
}
|
||||
|
||||
if (agentName) {
|
||||
await governanceService.recordAudit({
|
||||
agentName,
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ const BLOCKED_FILES = ['package.json', 'package-lock.json', '.env', 'drizzle.con
|
|||
const PROTECTED_PATHS = [
|
||||
'server/routes.ts',
|
||||
'shared/schema.ts',
|
||||
'shared/schemas/index.ts',
|
||||
'shared/schemas/loader.ts',
|
||||
'server/modules/loader.ts',
|
||||
'client/src/App.tsx',
|
||||
'client/src/main.tsx',
|
||||
'server/index.ts',
|
||||
|
|
@ -24,11 +21,6 @@ const PROTECTED_PATHS = [
|
|||
'db/index.ts',
|
||||
];
|
||||
|
||||
const ALLOWED_MODULE_DIRS = [
|
||||
'shared/schemas/',
|
||||
'server/modules/',
|
||||
];
|
||||
|
||||
export class WriteFileTool extends BaseTool {
|
||||
name = "write_file";
|
||||
description = "Escreve ou cria um arquivo no projeto";
|
||||
|
|
@ -61,6 +53,7 @@ export class WriteFileTool extends BaseTool {
|
|||
return this.formatError(`Arquivo protegido: ${fileName}`);
|
||||
}
|
||||
|
||||
// Bloquear sobrescrita de arquivos críticos do sistema
|
||||
if (PROTECTED_PATHS.includes(filePath)) {
|
||||
return this.formatError(`Arquivo crítico protegido - não pode ser sobrescrito: ${filePath}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -800,7 +800,7 @@ ${JSON.stringify(sampleData.slice(0, 10), null, 2)}
|
|||
Pergunta do usuário: ${question}`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "arcadia-agent",
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt }
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import type { Express, Request, Response } from "express";
|
|||
import multer from "multer";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
import AdmZip from "adm-zip";
|
||||
import * as BSON from "bson";
|
||||
import { db } from "../../db/index";
|
||||
import { biDatasets, stagedTables } from "@shared/schema";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
const uploadDir = path.join(process.cwd(), "uploads");
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
* Arcadia Suite - Base Blackboard Agent
|
||||
*
|
||||
* Classe base para agentes que monitoram e reagem ao Blackboard.
|
||||
* Toda inteligência de IA é centralizada via ManusIntelligence.
|
||||
*
|
||||
* @author Arcadia Development Team
|
||||
* @version 2.0.0 - Manus-Powered
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import OpenAI from "openai";
|
||||
|
|
@ -36,121 +35,6 @@ export interface AgentThought {
|
|||
result?: any;
|
||||
}
|
||||
|
||||
export class ManusIntelligence {
|
||||
private static instance: ManusIntelligence;
|
||||
private model = "arcadia-agent";
|
||||
private callCount = 0;
|
||||
private tokenCount = 0;
|
||||
private errorCount = 0;
|
||||
private lastCallAt = 0;
|
||||
private startedAt = Date.now();
|
||||
|
||||
static getInstance(): ManusIntelligence {
|
||||
if (!ManusIntelligence.instance) {
|
||||
ManusIntelligence.instance = new ManusIntelligence();
|
||||
}
|
||||
return ManusIntelligence.instance;
|
||||
}
|
||||
|
||||
async generate(systemPrompt: string, userPrompt: string, options?: { maxTokens?: number; temperature?: number; enrichContext?: boolean }): Promise<string> {
|
||||
this.callCount++;
|
||||
try {
|
||||
let enrichedPrompt = userPrompt;
|
||||
if (options?.enrichContext !== false) {
|
||||
const contextData = await this.enrichWithContext(userPrompt);
|
||||
if (contextData) {
|
||||
enrichedPrompt = userPrompt + contextData;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: enrichedPrompt }
|
||||
],
|
||||
max_tokens: options?.maxTokens || 8000,
|
||||
temperature: options?.temperature || 0.2,
|
||||
});
|
||||
const content = response.choices[0]?.message?.content || '';
|
||||
this.tokenCount += response.usage?.total_tokens || 0;
|
||||
this.lastCallAt = Date.now();
|
||||
return content;
|
||||
} catch (error: any) {
|
||||
this.errorCount++;
|
||||
console.error(`[ManusIntelligence] Erro na geração:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async think(systemPrompt: string, taskContext: string): Promise<AgentThought> {
|
||||
this.callCount++;
|
||||
try {
|
||||
const contextData = await this.enrichWithContext(taskContext);
|
||||
const enrichedContext = contextData ? taskContext + contextData : taskContext;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: enrichedContext }
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
max_tokens: 4000,
|
||||
temperature: 0.2,
|
||||
});
|
||||
const content = response.choices[0]?.message?.content || '{}';
|
||||
this.tokenCount += response.usage?.total_tokens || 0;
|
||||
this.lastCallAt = Date.now();
|
||||
return JSON.parse(content) as AgentThought;
|
||||
} catch (error: any) {
|
||||
this.errorCount++;
|
||||
console.error(`[ManusIntelligence] Erro ao pensar:`, error.message);
|
||||
return {
|
||||
thought: `Erro ao processar: ${error.message}`,
|
||||
finished: true,
|
||||
result: { error: error.message }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async enrichWithContext(prompt: string): Promise<string> {
|
||||
try {
|
||||
const { toolManager } = await import("../autonomous/tools");
|
||||
|
||||
const keywords = prompt.split(/\s+/).filter(w => w.length > 4).slice(0, 5);
|
||||
let contextData = "";
|
||||
|
||||
const searchResult = await toolManager.execute("search_code", {
|
||||
query: keywords.join("|"),
|
||||
maxResults: 5,
|
||||
});
|
||||
if (searchResult.success && searchResult.data?.results?.length > 0) {
|
||||
contextData += "\n\nCÓDIGO RELEVANTE DO PROJETO:\n" +
|
||||
searchResult.data.results.map((r: any) => `${r.file}:${r.line} - ${r.content}`).join("\n");
|
||||
}
|
||||
|
||||
return contextData;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return {
|
||||
model: this.model,
|
||||
callCount: this.callCount,
|
||||
tokenCount: this.tokenCount,
|
||||
errorCount: this.errorCount,
|
||||
lastCallAt: this.lastCallAt,
|
||||
startedAt: this.startedAt,
|
||||
healthy: !!process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const manusIntelligence = ManusIntelligence.getInstance();
|
||||
|
||||
export abstract class BaseBlackboardAgent extends EventEmitter {
|
||||
protected config: AgentConfig;
|
||||
protected isRunning: boolean = false;
|
||||
|
|
@ -182,17 +66,48 @@ export abstract class BaseBlackboardAgent extends EventEmitter {
|
|||
? `\n\nArtefatos disponíveis:\n${artifacts.map(a => `- ${a.name} (${a.type}): ${a.content?.slice(0, 500)}...`).join('\n')}`
|
||||
: '';
|
||||
|
||||
const taskContext = `TAREFA: ${task.title}\n\nDESCRIÇÃO: ${task.description}\n\nCONTEXTO: ${context}${artifactContext}\n\nResponda em JSON: { "thought": "seu raciocínio", "action": "próxima ação", "actionInput": {}, "finished": true/false, "result": "se finished=true" }`;
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: this.config.systemPrompt },
|
||||
{
|
||||
role: "user",
|
||||
content: `TAREFA: ${task.title}\n\nDESCRIÇÃO: ${task.description}\n\nCONTEXTO: ${context}${artifactContext}\n\nResponda em JSON: { "thought": "seu raciocínio", "action": "próxima ação", "actionInput": {}, "finished": true/false, "result": "se finished=true" }`
|
||||
}
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
max_tokens: 4000,
|
||||
});
|
||||
|
||||
return manusIntelligence.think(this.config.systemPrompt, taskContext);
|
||||
const content = response.choices[0]?.message?.content || '{}';
|
||||
return JSON.parse(content) as AgentThought;
|
||||
} catch (error: any) {
|
||||
console.error(`[${this.name}] Erro ao pensar:`, error.message);
|
||||
return {
|
||||
thought: `Erro ao processar: ${error.message}`,
|
||||
finished: true,
|
||||
result: { error: error.message }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async generateWithAI(prompt: string, systemPrompt?: string): Promise<string> {
|
||||
return manusIntelligence.generate(
|
||||
systemPrompt || this.config.systemPrompt,
|
||||
prompt,
|
||||
{ maxTokens: 8000 }
|
||||
);
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt || this.config.systemPrompt },
|
||||
{ role: "user", content: prompt }
|
||||
],
|
||||
max_tokens: 8000,
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content || '';
|
||||
} catch (error: any) {
|
||||
console.error(`[${this.name}] Erro na geração:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected async log(taskId: number, action: string, thought: string, observation?: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ const ALL_PHASES: PipelinePhase[] = ["design", "codegen", "validation", "staging
|
|||
|
||||
class PipelineOrchestrator extends EventEmitter {
|
||||
private activeMonitors: Map<number, NodeJS.Timeout> = new Map();
|
||||
private processingMonitors: Set<number> = new Set();
|
||||
|
||||
async createPipeline(prompt: string, userId: string = "system", metadata?: any): Promise<XosDevPipeline> {
|
||||
const correlationId = randomUUID();
|
||||
|
|
@ -169,10 +170,14 @@ class PipelineOrchestrator extends EventEmitter {
|
|||
if (this.activeMonitors.has(pipelineId)) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
if (this.processingMonitors.has(pipelineId)) return;
|
||||
this.processingMonitors.add(pipelineId);
|
||||
try {
|
||||
await this.checkPhaseProgress(pipelineId, mainTaskId);
|
||||
} catch (error) {
|
||||
console.error(`[PipelineOrchestrator] Erro no monitor #${pipelineId}:`, error);
|
||||
} finally {
|
||||
this.processingMonitors.delete(pipelineId);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
|
|
@ -384,26 +389,6 @@ class PipelineOrchestrator extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
const schemaFiles = applied.filter(f => f.startsWith("shared/schemas/") && f.endsWith(".ts"));
|
||||
if (schemaFiles.length > 0) {
|
||||
try {
|
||||
const { registerAndMigrate } = await import("../modules/migrator");
|
||||
for (const schemaFile of schemaFiles) {
|
||||
const moduleName = schemaFile.replace("shared/schemas/", "").replace(".ts", "");
|
||||
if (moduleName && moduleName !== "index" && moduleName !== "loader" && !moduleName.startsWith("_")) {
|
||||
const migResult = await registerAndMigrate(moduleName);
|
||||
if (migResult.success) {
|
||||
console.log(`[Pipeline] Módulo ${moduleName}: schema registrado${migResult.migrationApplied ? " + migração aplicada" : ""}`);
|
||||
} else {
|
||||
errors.push(`Migração ${moduleName}: ${migResult.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (migError: any) {
|
||||
console.error("[Pipeline] Erro no migrator:", migError.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (applied.length > 0) {
|
||||
await toolManager.execute("git_local_commit", {
|
||||
message: `[Pipeline #${pipelineId}] ${pipeline.prompt.slice(0, 60)}`,
|
||||
|
|
|
|||
|
|
@ -33,44 +33,18 @@ Você tem acesso ao contexto do projeto e pode ler arquivos existentes.
|
|||
- Banco: PostgreSQL
|
||||
- Autenticação: Passport.js
|
||||
|
||||
## SISTEMA MODULAR (IMPORTANTE!)
|
||||
O projeto usa um sistema modular para autonomia do Dev Center:
|
||||
|
||||
### Schemas (Banco de Dados)
|
||||
- Criar schemas novos em: shared/schemas/{nomeModulo}.ts
|
||||
- NÃO modificar shared/schema.ts (protegido)
|
||||
- NÃO importar de @shared/schema (evitar referência circular)
|
||||
- Usar varchar("user_id") SEM .references() para referências a tabelas do schema principal
|
||||
- Importar diretamente: import { sql } from "drizzle-orm"; import { pgTable, ... } from "drizzle-orm/pg-core";
|
||||
- Exportar: tabelas, insertSchemas (drizzle-zod), e types
|
||||
- Prefixar tabelas com nome do módulo: bsc_objectives, fin_transactions, etc.
|
||||
|
||||
### Rotas (API)
|
||||
- Criar rotas em: server/modules/{nomeModulo}.ts
|
||||
- NÃO modificar server/routes.ts (protegido)
|
||||
- Exportar default um Router do Express
|
||||
- As rotas ficam automaticamente em /api/modules/{nomeModulo}
|
||||
- Importar schemas do módulo: import { ... } from "@shared/schemas/{nomeModulo}"
|
||||
- Importar db: import { db } from "../../db"
|
||||
|
||||
### Páginas (Frontend)
|
||||
- Criar em: client/src/pages/{NomeModulo}.tsx
|
||||
- Componentes em: client/src/components/{nomeModulo}/
|
||||
|
||||
## Formato de Saída (JSON)
|
||||
{
|
||||
"moduleName": "nome-do-modulo",
|
||||
"description": "descrição completa",
|
||||
"schema": {
|
||||
"file": "shared/schemas/{moduleName}.ts",
|
||||
"tables": [{ "name": "tabela", "columns": [{ "name": "coluna", "type": "tipo", "constraints": [] }] }]
|
||||
},
|
||||
"api": {
|
||||
"file": "server/modules/{moduleName}.ts",
|
||||
"routes": [{ "method": "GET|POST|PUT|DELETE", "path": "/{rota}", "description": "descrição" }]
|
||||
"routes": [{ "method": "GET|POST|PUT|DELETE", "path": "/api/rota", "description": "descrição", "requestBody": {}, "response": {} }]
|
||||
},
|
||||
"ui": {
|
||||
"components": [{ "name": "Componente", "type": "page|modal|widget", "description": "desc", "path": "client/src/pages/{Nome}.tsx" }]
|
||||
"components": [{ "name": "Componente", "type": "page|modal|widget", "description": "desc", "path": "caminho/arquivo.tsx" }]
|
||||
},
|
||||
"integrations": ["módulos existentes que devem ser integrados"],
|
||||
"existingFiles": ["arquivos que precisam ser modificados"]
|
||||
|
|
|
|||
|
|
@ -35,9 +35,6 @@ const PROTECTED_FILES = [
|
|||
"server/storage.ts",
|
||||
"server/db.ts",
|
||||
"shared/schema.ts",
|
||||
"shared/schemas/index.ts",
|
||||
"shared/schemas/loader.ts",
|
||||
"server/modules/loader.ts",
|
||||
"client/src/App.tsx",
|
||||
"client/src/main.tsx",
|
||||
"client/src/pages/Cockpit.tsx",
|
||||
|
|
@ -48,21 +45,8 @@ const PROTECTED_FILES = [
|
|||
"tsconfig.json",
|
||||
"vite.config.ts",
|
||||
"drizzle.config.ts",
|
||||
"db/index.ts",
|
||||
];
|
||||
|
||||
const ALLOWED_MODULE_DIRS = [
|
||||
"shared/schemas/",
|
||||
"server/modules/",
|
||||
];
|
||||
|
||||
export function isModuleFile(filePath: string): boolean {
|
||||
return ALLOWED_MODULE_DIRS.some(dir => filePath.startsWith(dir)) &&
|
||||
!PROTECTED_FILES.includes(filePath) &&
|
||||
!filePath.startsWith("shared/schemas/_") &&
|
||||
!filePath.startsWith("server/modules/_");
|
||||
}
|
||||
|
||||
export class ExecutorAgent extends BaseBlackboardAgent {
|
||||
constructor() {
|
||||
const config: AgentConfig = {
|
||||
|
|
|
|||
|
|
@ -34,27 +34,6 @@ Você pode ler arquivos existentes para manter consistência de estilo.
|
|||
5. Use shadcn/ui para componentes de UI
|
||||
6. Adicione data-testid em elementos interativos
|
||||
|
||||
## SISTEMA MODULAR (IMPORTANTE!)
|
||||
O projeto usa um sistema modular. SEMPRE crie arquivos nas pastas modulares:
|
||||
|
||||
### Schemas de Banco → shared/schemas/{nomeModulo}.ts
|
||||
- NÃO modificar shared/schema.ts (protegido!)
|
||||
- NÃO importar de @shared/schema (referência circular!)
|
||||
- Importar: import { sql } from "drizzle-orm"; import { pgTable, text, varchar, serial, integer, timestamp, boolean, jsonb, numeric } from "drizzle-orm/pg-core";
|
||||
- Importar: import { createInsertSchema } from "drizzle-zod"; import { z } from "zod";
|
||||
- Para referências a users: usar varchar("user_id") SEM .references()
|
||||
- Prefixar tabelas: bsc_objectives, fin_transactions, etc.
|
||||
- Exportar: tabelas + insertSchemas + types
|
||||
|
||||
### Rotas de API → server/modules/{nomeModulo}.ts
|
||||
- NÃO modificar server/routes.ts (protegido!)
|
||||
- Exportar default um Router do Express
|
||||
- Rotas montadas automaticamente em /api/modules/{nomeModulo}
|
||||
- Import db: import { db } from "../../db";
|
||||
- Import schemas: import { ... } from "@shared/schemas/{nomeModulo}";
|
||||
|
||||
### Páginas React → client/src/pages/{NomeModulo}.tsx
|
||||
|
||||
## Formato de Saída
|
||||
Para cada arquivo, retorne JSON:
|
||||
{
|
||||
|
|
@ -138,8 +117,8 @@ ${schemaRef}
|
|||
${routesRef}
|
||||
|
||||
Gere arquivos completos e funcionais para:
|
||||
1. Schema do banco (se necessário) - CRIAR em shared/schemas/{moduleName}.ts (NÃO editar schema.ts!)
|
||||
2. Rotas da API - CRIAR em server/modules/{moduleName}.ts (NÃO editar routes.ts!)
|
||||
1. Schema do banco de dados (se necessário) - adicionar ao shared/schema.ts
|
||||
2. Rotas da API - novo arquivo em server/ ou adicionar a routes.ts
|
||||
3. Componente principal React - em client/src/pages/
|
||||
|
||||
Retorne em formato JSON com a estrutura:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { Router, Request, Response } from "express";
|
||||
import { pipelineOrchestrator } from "./PipelineOrchestrator";
|
||||
import { toolManager } from "../autonomous/tools/ToolManager";
|
||||
import { z } from "zod";
|
||||
|
||||
const router = Router();
|
||||
|
||||
const createPipelineSchema = z.object({
|
||||
prompt: z.string().min(5, "O prompt deve ter pelo menos 5 caracteres"),
|
||||
mode: z.enum(["plan", "act"]).optional().default("act"),
|
||||
planContext: z.string().optional(),
|
||||
metadata: z.record(z.any()).optional(),
|
||||
budget: z.object({
|
||||
maxTokens: z.number().optional(),
|
||||
|
|
@ -24,64 +21,18 @@ router.post("/", async (req: Request, res: Response) => {
|
|||
return res.status(400).json({ success: false, error: parsed.error.errors[0].message });
|
||||
}
|
||||
|
||||
const mode = parsed.data.mode || "act";
|
||||
const userId = (req.user as any)?.id || "anonymous";
|
||||
const metadata = {
|
||||
...(parsed.data.metadata || {}),
|
||||
budget: parsed.data.budget,
|
||||
mode,
|
||||
planContext: parsed.data.planContext || null,
|
||||
};
|
||||
|
||||
let prompt = parsed.data.prompt;
|
||||
if (mode === "plan") {
|
||||
prompt = `[MODO PLANEJAMENTO] Analise o projeto e gere um plano detalhado de implementação. NÃO modifique arquivos. Use apenas ferramentas de leitura (read_file, search_code, list_directory). Gere um documento implementation_plan.md com:\n1. Análise do estado atual\n2. Arquivos que precisam ser criados/modificados\n3. Estratégia passo-a-passo\n4. Dependências e riscos\n\nSolicitação: ${prompt}`;
|
||||
} else if (parsed.data.planContext) {
|
||||
prompt = `[MODO EXECUÇÃO] Siga o plano de implementação abaixo e execute as mudanças:\n\n--- PLANO ---\n${parsed.data.planContext}\n--- FIM DO PLANO ---\n\nSolicitação original: ${prompt}`;
|
||||
}
|
||||
|
||||
const pipeline = await pipelineOrchestrator.createPipeline(prompt, userId, metadata);
|
||||
|
||||
toolManager.setActivePipeline(pipeline.id);
|
||||
toolManager.setPlanMode(mode === "plan", pipeline.id);
|
||||
|
||||
const metadata = { ...(parsed.data.metadata || {}), budget: parsed.data.budget };
|
||||
const pipeline = await pipelineOrchestrator.createPipeline(parsed.data.prompt, userId, metadata);
|
||||
const started = await pipelineOrchestrator.startPipeline(pipeline.id);
|
||||
|
||||
res.json({ success: true, pipeline: started, mode });
|
||||
res.json({ success: true, pipeline: started });
|
||||
} catch (error: any) {
|
||||
console.error("[Pipeline] Erro ao criar:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/agent-terminal", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const pipelineId = req.query.pipelineId ? parseInt(req.query.pipelineId as string) : undefined;
|
||||
const logs = toolManager.getCommandLog(pipelineId);
|
||||
res.json({ success: true, logs });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/mode", async (req: Request, res: Response) => {
|
||||
const pipelineId = req.query.pipelineId ? parseInt(req.query.pipelineId as string) : undefined;
|
||||
if (pipelineId !== undefined) {
|
||||
toolManager.setActivePipeline(pipelineId);
|
||||
}
|
||||
res.json({ success: true, planMode: toolManager.planMode, activePipeline: toolManager.activePipelineId });
|
||||
});
|
||||
|
||||
router.post("/mode", async (req: Request, res: Response) => {
|
||||
const { mode, pipelineId } = req.body;
|
||||
if (mode !== "plan" && mode !== "act") {
|
||||
return res.status(400).json({ success: false, error: "Mode must be 'plan' or 'act'" });
|
||||
}
|
||||
const pid = pipelineId ? parseInt(pipelineId) : undefined;
|
||||
toolManager.setPlanMode(mode === "plan", pid);
|
||||
res.json({ success: true, mode, planMode: toolManager.planMode });
|
||||
});
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 50);
|
||||
|
|
@ -235,29 +186,4 @@ router.get("/:id/stream", async (req: Request, res: Response) => {
|
|||
});
|
||||
});
|
||||
|
||||
router.get("/modules/status", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const { getModuleStatus, listModuleSchemas } = await import("../modules/migrator");
|
||||
const { getLoadedModules } = await import("../modules/loader");
|
||||
|
||||
const status = await getModuleStatus();
|
||||
const schemas = await listModuleSchemas();
|
||||
const loadedRoutes = getLoadedModules();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
modules: status,
|
||||
registeredSchemas: schemas,
|
||||
loadedRoutes,
|
||||
allowedPaths: {
|
||||
schemas: "shared/schemas/{moduleName}.ts",
|
||||
routes: "server/modules/{moduleName}.ts",
|
||||
pages: "client/src/pages/{ModuleName}.tsx",
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function requireAuth(req: Request, res: Response, next: NextFunction) {
|
|||
// Apply auth to all routes
|
||||
router.use(requireAuth);
|
||||
|
||||
// Helper to get tenant ID from request header or user's first tenant (auto-creates if none)
|
||||
// Helper to get tenant ID from request header or user's first tenant
|
||||
async function getTenantId(req: Request): Promise<number | null> {
|
||||
const userId = (req.user as any).id;
|
||||
const headerTenantId = req.headers["x-tenant-id"];
|
||||
|
|
@ -40,13 +40,8 @@ async function getTenantId(req: Request): Promise<number | null> {
|
|||
const isMember = await compassStorage.isUserInTenant(userId, tenantId);
|
||||
return isMember ? tenantId : null;
|
||||
}
|
||||
const userTenants = await compassStorage.getUserTenants(userId);
|
||||
if (userTenants.length > 0) return userTenants[0].id;
|
||||
// Auto-cria tenant padrão para o usuário na primeira vez
|
||||
const userName = (req.user as any).name || (req.user as any).username || "Minha Organização";
|
||||
const tenant = await compassStorage.createTenant({ name: userName, plan: "free", status: "active" });
|
||||
await compassStorage.addUserToTenant({ tenantId: tenant.id, userId, role: "owner", isOwner: "true" });
|
||||
return tenant.id;
|
||||
const tenants = await compassStorage.getUserTenants(userId);
|
||||
return tenants.length > 0 ? tenants[0].id : null;
|
||||
}
|
||||
|
||||
// Validate tenant membership
|
||||
|
|
@ -2279,4 +2274,124 @@ router.put("/projects/:projectId/history", async (req: Request, res: Response) =
|
|||
}
|
||||
});
|
||||
|
||||
// ========== AI BRIEFING & HEALTH SCORE ==========
|
||||
|
||||
router.post("/projects/:projectId/ai-brief", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tenantId = await getTenantId(req);
|
||||
if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" });
|
||||
const projectId = parseInt(req.params.projectId);
|
||||
const hasAccess = await validateProjectAccess(projectId, tenantId);
|
||||
if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" });
|
||||
|
||||
const project = await compassStorage.getProject(projectId, tenantId);
|
||||
const tasks = await compassStorage.getTasks(projectId);
|
||||
const pdcaCycles = await compassStorage.getPdcaCycles(tenantId, projectId);
|
||||
|
||||
const openTasks = tasks.filter((t: any) => t.status !== "done" && t.status !== "completed");
|
||||
const overdue = tasks.filter((t: any) => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== "done");
|
||||
const pdcaOpen = pdcaCycles.filter((c: any) => c.status === "open" || c.status === "in_progress");
|
||||
|
||||
const OpenAI = (await import("openai")).default;
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const prompt = `Você é um consultor de gestão de projetos. Analise o projeto abaixo e gere um briefing executivo em português.
|
||||
|
||||
PROJETO: ${project?.name}
|
||||
DESCRIÇÃO: ${project?.description || "Não informada"}
|
||||
STATUS: ${project?.status}
|
||||
FASE: ${project?.currentPhase || "Não definida"}
|
||||
|
||||
RESUMO DE TAREFAS:
|
||||
- Total: ${tasks.length}
|
||||
- Em aberto: ${openTasks.length}
|
||||
- Atrasadas: ${overdue.length}
|
||||
|
||||
PDCA ATIVOS: ${pdcaOpen.length}
|
||||
|
||||
${overdue.length > 0 ? `TAREFAS ATRASADAS:\n${overdue.slice(0, 5).map((t: any) => `- ${t.title} (responsável: ${t.assignedTo || "não atribuído"})`).join("\n")}` : ""}
|
||||
|
||||
Gere um briefing com:
|
||||
1. **Situação atual** (2-3 linhas)
|
||||
2. **Principais riscos** (lista com até 3 itens)
|
||||
3. **Ações prioritárias** (lista com até 5 ações, cada uma com responsável sugerido e prazo)
|
||||
4. **Score de saúde** (0-100 com justificativa)
|
||||
|
||||
Seja direto e objetivo.`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
max_tokens: 800,
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const briefText = response.choices[0]?.message?.content || "";
|
||||
|
||||
// Extract health score from response
|
||||
const scoreMatch = briefText.match(/score[^\d]*(\d{1,3})/i) || briefText.match(/(\d{1,3})[^\d]*\//);
|
||||
const healthScore = scoreMatch ? Math.min(100, Math.max(0, parseInt(scoreMatch[1]))) : null;
|
||||
|
||||
res.json({
|
||||
projectId,
|
||||
brief: briefText,
|
||||
healthScore,
|
||||
meta: {
|
||||
totalTasks: tasks.length,
|
||||
openTasks: openTasks.length,
|
||||
overdueTasks: overdue.length,
|
||||
activePdca: pdcaOpen.length,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/projects/:projectId/health", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tenantId = await getTenantId(req);
|
||||
if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" });
|
||||
const projectId = parseInt(req.params.projectId);
|
||||
const hasAccess = await validateProjectAccess(projectId, tenantId);
|
||||
if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" });
|
||||
|
||||
const tasks = await compassStorage.getTasks(projectId);
|
||||
const pdcaCycles = await compassStorage.getPdcaCycles(tenantId, projectId);
|
||||
|
||||
const total = tasks.length || 1;
|
||||
const done = tasks.filter((t: any) => t.status === "done" || t.status === "completed").length;
|
||||
const overdue = tasks.filter((t: any) => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== "done").length;
|
||||
const pdcaComplete = pdcaCycles.filter((c: any) => c.status === "completed").length;
|
||||
const pdcaTotal = pdcaCycles.length || 1;
|
||||
|
||||
const completionScore = (done / total) * 40;
|
||||
const overdueScore = Math.max(0, 30 - (overdue / total) * 30);
|
||||
const pdcaScore = (pdcaComplete / pdcaTotal) * 30;
|
||||
const healthScore = Math.round(completionScore + overdueScore + pdcaScore);
|
||||
|
||||
const level = healthScore >= 80 ? "saudável" : healthScore >= 50 ? "atenção" : "crítico";
|
||||
const color = healthScore >= 80 ? "green" : healthScore >= 50 ? "yellow" : "red";
|
||||
|
||||
res.json({
|
||||
projectId,
|
||||
healthScore,
|
||||
level,
|
||||
color,
|
||||
breakdown: {
|
||||
completion: { score: Math.round(completionScore), weight: "40%", tasks: `${done}/${tasks.length}` },
|
||||
timeliness: { score: Math.round(overdueScore), weight: "30%", overdue },
|
||||
pdca: { score: Math.round(pdcaScore), weight: "30%", cycles: `${pdcaComplete}/${pdcaCycles.length}` },
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export class FrappeService {
|
|||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(30000),
|
||||
};
|
||||
|
||||
if (data && (method === "POST" || method === "PUT")) {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { manusService } from "./manus/service";
|
|||
import { db } from "../db/index";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
|
||||
|
||||
const router = Router();
|
||||
|
||||
const DEV_AGENT_CONTEXT = `CONTEXTO ESPECIAL - DEV AGENT:
|
||||
|
|
@ -79,7 +77,9 @@ Usuário: ${message}
|
|||
|
||||
Responda como Dev Agent, criando os recursos solicitados:`;
|
||||
|
||||
const responseContent = await manusService.runSync(userId, fullPrompt, []);
|
||||
const result = await manusService.run(userId, fullPrompt, []);
|
||||
|
||||
const responseContent = result.finalResponse || "Desculpe, não consegui processar sua solicitação.";
|
||||
|
||||
const jsonMatch = responseContent.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
let action = null;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import type { Express, Request, Response } from "express";
|
||||
import { restartManagedService, stopManagedService, getManagedServiceInfo, getManagedServiceLogs } from "../index";
|
||||
import { manusIntelligence } from "../blackboard/BaseBlackboardAgent";
|
||||
|
||||
interface EngineConfig {
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: "python" | "php" | "node" | "java" | "ai";
|
||||
type: "python" | "php" | "node" | "java";
|
||||
port: number;
|
||||
healthPath: string;
|
||||
category: "erp" | "intelligence" | "data" | "fiscal" | "automation";
|
||||
|
|
@ -13,23 +12,14 @@ interface EngineConfig {
|
|||
}
|
||||
|
||||
const ENGINES: EngineConfig[] = [
|
||||
{
|
||||
name: "manus-ia",
|
||||
displayName: "Manus IA (Cerebro Central)",
|
||||
type: "ai",
|
||||
port: 5000,
|
||||
healthPath: "/api/manus/health",
|
||||
category: "intelligence",
|
||||
description: "Motor de IA Central - GPT-4o, 56 Tools, Knowledge Graph, Dev Pipeline",
|
||||
},
|
||||
{
|
||||
name: "plus",
|
||||
displayName: "Arcadia Plus (ERP)",
|
||||
type: "php",
|
||||
port: 8080,
|
||||
healthPath: "/",
|
||||
healthPath: "/api/health",
|
||||
category: "erp",
|
||||
description: "Motor Plus - PDV, NF-e, Estoque, Financeiro",
|
||||
description: "ERP completo Laravel - PDV, NF-e, Estoque, Financeiro",
|
||||
},
|
||||
{
|
||||
name: "contabil",
|
||||
|
|
@ -94,21 +84,12 @@ async function checkEngineHealth(engine: EngineConfig): Promise<any> {
|
|||
|
||||
try {
|
||||
const start = Date.now();
|
||||
const headers: Record<string, string> = {};
|
||||
if (engine.name === "manus-ia") {
|
||||
headers["x-internal-check"] = "engine-room";
|
||||
}
|
||||
const fetchOptions: any = { signal: controller.signal, headers };
|
||||
if (engine.name === "plus") {
|
||||
fetchOptions.redirect = "manual";
|
||||
}
|
||||
const response = await fetch(url, fetchOptions);
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
const isRedirect = response.status >= 300 && response.status < 400;
|
||||
if (response.ok || (engine.name === "plus" && isRedirect)) {
|
||||
const data = response.ok ? await response.json().catch(() => ({})) : { redirect: true, location: response.headers.get("location") };
|
||||
if (response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
return {
|
||||
...engine,
|
||||
status: "online",
|
||||
|
|
@ -399,60 +380,5 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
}
|
||||
});
|
||||
|
||||
app.get("/api/manus/health", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const isInternal = req.headers["x-internal-check"] === "engine-room";
|
||||
if (!isInternal && !req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
const metrics = manusIntelligence.getMetrics();
|
||||
|
||||
let toolCount = 0;
|
||||
try {
|
||||
const { toolManager } = await import("../autonomous/tools");
|
||||
toolCount = (toolManager as any).getToolNames?.()?.length || (toolManager as any).tools?.size || 56;
|
||||
} catch {
|
||||
toolCount = 56;
|
||||
}
|
||||
|
||||
let agentCount = 0;
|
||||
try {
|
||||
const { getAgentsStatus } = await import("../blackboard/agents");
|
||||
const agents = getAgentsStatus();
|
||||
agentCount = agents.filter((a: any) => a.running).length;
|
||||
} catch {}
|
||||
|
||||
const aiConfigured = metrics.healthy;
|
||||
const engineStatus = aiConfigured ? "online" : "error";
|
||||
|
||||
res.json({
|
||||
status: engineStatus,
|
||||
engine: "Manus IA",
|
||||
version: "2.0.0",
|
||||
model: metrics.model,
|
||||
database: "connected",
|
||||
aiConfigured,
|
||||
capabilities: {
|
||||
tools: toolCount,
|
||||
agents: agentCount,
|
||||
knowledgeGraph: true,
|
||||
semanticSearch: true,
|
||||
devPipeline: true,
|
||||
autonomousDev: true,
|
||||
},
|
||||
metrics: {
|
||||
totalCalls: metrics.callCount,
|
||||
totalTokens: metrics.tokenCount,
|
||||
errorCount: metrics.errorCount,
|
||||
lastCallAt: metrics.lastCallAt || null,
|
||||
uptime: process.uptime(),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ status: "error", error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[Engine Room] Rotas registradas em /api/engine-room/*");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,12 @@
|
|||
import type { Express, Request, Response } from "express";
|
||||
import { erpStorage, createErpClient } from "./index";
|
||||
import { db } from "../../db/index";
|
||||
import { customers, suppliers, products, salesOrders, purchaseOrders, erpSegments, erpConfig, insertErpSegmentSchema, insertErpConfigSchema, persons, personRoles, mobileDevices, posSales, posSaleItems, finAccountsReceivable, tenants, tenantEmpresas, type TenantFeatures } from "@shared/schema";
|
||||
import { customers, suppliers, products, salesOrders, purchaseOrders, erpSegments, erpConfig, insertErpSegmentSchema, insertErpConfigSchema, persons, personRoles, mobileDevices, posSales, posSaleItems, finAccountsReceivable } from "@shared/schema";
|
||||
import { eq, and, sql, desc } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { soeRuleEngine } from "../soe/rule-engine/index";
|
||||
import { soeEventBus } from "../soe/event-bus";
|
||||
|
||||
export function registerSoeRoutes(app: Express): void {
|
||||
app.use((req, res, next) => {
|
||||
if (req.url.startsWith("/api/soe/") || req.url === "/api/soe") {
|
||||
req.url = req.url.replace("/api/soe", "/api/erp");
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
registerErpRoutes(app);
|
||||
}
|
||||
|
||||
export function registerErpRoutes(app: Express): void {
|
||||
app.get("/api/erp/connections", async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
|
@ -863,23 +852,24 @@ export function registerErpRoutes(app: Express): void {
|
|||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
const tenantId = req.user?.tenantId || 1;
|
||||
const user = req.user as any;
|
||||
|
||||
// SOE Rule Engine — enriquece payload antes de inserir
|
||||
const enriched = await soeRuleEngine.apply("pre_sales_order", req.body, {
|
||||
tenantId,
|
||||
motor: "local",
|
||||
});
|
||||
if (enriched.validationErrors.length > 0) {
|
||||
return res.status(422).json({ error: "Validação SOE falhou", detalhes: enriched.validationErrors });
|
||||
// SOE Rule Engine — aplica regras fiscais e de negócio antes de criar o pedido
|
||||
const { payload: enriched, validationErrors } = await soeRuleEngine.apply(
|
||||
"pre_sales_order",
|
||||
{ ...req.body, _tipo: "venda" },
|
||||
{ tenantId, empresaId: req.body.empresaId, userId: user?.id, motor: "local" }
|
||||
);
|
||||
if (validationErrors.length > 0) {
|
||||
return res.status(422).json({ error: "Validação falhou", details: validationErrors });
|
||||
}
|
||||
const body = enriched.payload;
|
||||
|
||||
const { orderNumber, customerId, orderDate, deliveryDate, status, subtotal, discount, tax, total, paymentMethod, notes } = body;
|
||||
const { orderNumber, customerId, orderDate, deliveryDate, status, subtotal, discount, tax, total, paymentMethod, notes } = enriched;
|
||||
const [order] = await db.insert(salesOrders).values({
|
||||
tenantId,
|
||||
orderNumber, customerId, orderDate, deliveryDate, status, subtotal, discount, tax, total, paymentMethod, notes
|
||||
}).returning();
|
||||
res.status(201).json(order);
|
||||
res.status(201).json({ ...order, _rulesApplied: enriched._rulesApplied });
|
||||
} catch (error) {
|
||||
console.error("Error creating sales order:", error);
|
||||
res.status(500).json({ error: "Failed to create sales order" });
|
||||
|
|
@ -900,7 +890,16 @@ export function registerErpRoutes(app: Express): void {
|
|||
if (!updated) {
|
||||
return res.status(404).json({ error: "Sales order not found" });
|
||||
}
|
||||
res.json({ success: true, order: updated, message: "Pedido confirmado. Lançamentos contábeis gerados." });
|
||||
|
||||
// SOE Event Bus — dispara automações pós-confirmação
|
||||
soeEventBus.emit("venda_confirmada", {
|
||||
tenantId,
|
||||
empresaId: (req.user as any)?.empresaId,
|
||||
motorOrigem: "local",
|
||||
dados: { id, status: "confirmed", ...updated },
|
||||
}).catch((e) => console.error("[SOE] venda_confirmada error:", e.message));
|
||||
|
||||
res.json({ success: true, order: updated, message: "Pedido confirmado. Automações SOE disparadas." });
|
||||
} catch (error) {
|
||||
console.error("Error confirming sales order:", error);
|
||||
res.status(500).json({ error: "Failed to confirm sales order" });
|
||||
|
|
@ -914,14 +913,16 @@ export function registerErpRoutes(app: Express): void {
|
|||
}
|
||||
const id = parseInt(req.params.id);
|
||||
const tenantId = req.user?.tenantId || 1;
|
||||
const user = req.user as any;
|
||||
|
||||
// SOE Rule Engine — valida e enriquece payload fiscal antes de emitir
|
||||
const enriched = await soeRuleEngine.apply("pre_nfe_emissao", { ...req.body, _pedidoId: id }, {
|
||||
tenantId,
|
||||
motor: "local",
|
||||
});
|
||||
if (enriched.validationErrors.length > 0) {
|
||||
return res.status(422).json({ error: "Validação fiscal SOE falhou", detalhes: enriched.validationErrors });
|
||||
// SOE Rule Engine — aplica regras fiscais antes de gerar a NF-e
|
||||
const { payload: enriched, validationErrors } = await soeRuleEngine.apply(
|
||||
"pre_nfe_emissao",
|
||||
{ ...req.body, tipoDocumento: "nfe", _tipo: "venda" },
|
||||
{ tenantId, empresaId: user?.empresaId, userId: user?.id, motor: "local" }
|
||||
);
|
||||
if (validationErrors.length > 0) {
|
||||
return res.status(422).json({ error: "Validação fiscal falhou", details: validationErrors });
|
||||
}
|
||||
|
||||
const [updated] = await db.update(salesOrders)
|
||||
|
|
@ -935,12 +936,22 @@ export function registerErpRoutes(app: Express): void {
|
|||
// SOE Event Bus — dispara lançamentos contábeis automáticos
|
||||
soeEventBus.emit("nfe_emitida", {
|
||||
tenantId,
|
||||
empresaId: enriched.payload.empresaId || req.body.empresaId,
|
||||
empresaId: user?.empresaId,
|
||||
motorOrigem: "local",
|
||||
dados: { ...enriched.payload, ...updated },
|
||||
}).catch((e: any) => console.error("[routes] eventBus nfe_emitida:", e.message));
|
||||
dados: {
|
||||
...enriched,
|
||||
...updated,
|
||||
valorTotal: updated.total,
|
||||
dataEmissao: new Date().toISOString(),
|
||||
},
|
||||
}).catch((e) => console.error("[SOE] nfe_emitida error:", e.message));
|
||||
|
||||
res.json({ success: true, order: updated, regrasSoe: enriched.appliedRules, message: "NF-e gerada com sucesso." });
|
||||
res.json({
|
||||
success: true,
|
||||
order: updated,
|
||||
tributacao: enriched.tributacao,
|
||||
message: "NF-e gerada. Lançamentos contábeis sendo processados.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating NF-e:", error);
|
||||
res.status(500).json({ error: "Failed to generate NF-e" });
|
||||
|
|
@ -997,15 +1008,7 @@ export function registerErpRoutes(app: Express): void {
|
|||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
const tenantId = req.user?.tenantId || 1;
|
||||
|
||||
// SOE Rule Engine — enriquece payload de compra (CFOP, tributação entrada)
|
||||
const enriched = await soeRuleEngine.apply("pre_purchase_order", req.body, {
|
||||
tenantId,
|
||||
motor: "local",
|
||||
});
|
||||
const body = enriched.payload;
|
||||
|
||||
const { orderNumber, supplierId, orderDate, expectedDate, status, subtotal, discount, tax, total, notes } = body;
|
||||
const { orderNumber, supplierId, orderDate, expectedDate, status, subtotal, discount, tax, total, notes } = req.body;
|
||||
const [order] = await db.insert(purchaseOrders).values({
|
||||
tenantId,
|
||||
orderNumber, supplierId, orderDate, expectedDate, status, subtotal, discount, tax, total, notes
|
||||
|
|
@ -1186,198 +1189,7 @@ export function registerErpRoutes(app: Express): void {
|
|||
res.status(500).json({ error: "Failed to save config" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/erp/tenant/modules - Get current tenant's active modules
|
||||
app.get("/api/erp/tenant/modules", async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||
const tenantId = req.user?.tenantId || 1;
|
||||
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, tenantId));
|
||||
if (!tenant) return res.status(404).json({ error: "Tenant not found" });
|
||||
|
||||
const defaultFeatures: TenantFeatures = {
|
||||
ide: true, ideMode: 'pro-code', whatsapp: false, whatsappSessions: 0,
|
||||
crm: true, erp: true, bi: false, manus: true, manusTools: [],
|
||||
centralApis: false, centralApisManage: false, comunidades: false,
|
||||
maxChannels: 5, biblioteca: false, bibliotecaPublish: false,
|
||||
suporteN3: false, retail: false, plus: false, fisco: false,
|
||||
cockpit: false, compass: true, production: false, support: false, xosCrm: false
|
||||
};
|
||||
|
||||
res.json({
|
||||
tenantId: tenant.id,
|
||||
tenantName: tenant.name,
|
||||
plan: tenant.plan,
|
||||
features: tenant.features || defaultFeatures
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching tenant modules:", error);
|
||||
res.status(500).json({ error: "Failed to fetch modules" });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/erp/tenant/modules - Update current tenant's modules
|
||||
app.put("/api/erp/tenant/modules", async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||
const user = req.user;
|
||||
if (user?.role !== 'admin' && (user as any)?.tenantRole !== 'owner') {
|
||||
return res.status(403).json({ error: "Only admins can manage modules" });
|
||||
}
|
||||
const tenantId = user?.tenantId || 1;
|
||||
|
||||
const features = req.body.features;
|
||||
if (!features || typeof features !== 'object') {
|
||||
return res.status(400).json({ error: "Invalid features object" });
|
||||
}
|
||||
|
||||
const [updated] = await db.update(tenants)
|
||||
.set({ features, updatedAt: new Date() })
|
||||
.where(eq(tenants.id, tenantId))
|
||||
.returning();
|
||||
|
||||
res.json({ tenantId: updated.id, features: updated.features });
|
||||
} catch (error) {
|
||||
console.error("Error updating tenant modules:", error);
|
||||
res.status(500).json({ error: "Failed to update modules" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/erp/sync/status - Get sync status for all motors
|
||||
app.get("/api/erp/sync/status", async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||
const tenantId = req.user?.tenantId || 1;
|
||||
|
||||
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, tenantId));
|
||||
const features = (tenant?.features as TenantFeatures) || {};
|
||||
|
||||
const status = {
|
||||
retail: {
|
||||
enabled: features.retail || false,
|
||||
motor: features.plus ? 'plus' : features.erp ? 'erpnext' : 'none',
|
||||
lastSync: null,
|
||||
status: 'idle'
|
||||
},
|
||||
plus: {
|
||||
enabled: features.plus || false,
|
||||
status: 'idle'
|
||||
},
|
||||
fisco: {
|
||||
enabled: features.fisco || false,
|
||||
status: 'idle'
|
||||
},
|
||||
bi: {
|
||||
enabled: features.bi || false,
|
||||
status: 'idle'
|
||||
}
|
||||
};
|
||||
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
console.error("Error fetching sync status:", error);
|
||||
res.status(500).json({ error: "Failed to fetch sync status" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/erp/empresas", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tenantId = (req as any).user?.tenantId;
|
||||
if (!tenantId) return res.status(401).json({ error: "Tenant not identified" });
|
||||
const empresas = await db.select().from(tenantEmpresas)
|
||||
.where(and(eq(tenantEmpresas.tenantId, tenantId), eq(tenantEmpresas.status, "active")));
|
||||
res.json(empresas);
|
||||
} catch (error) {
|
||||
console.error("Error fetching empresas:", error);
|
||||
res.status(500).json({ error: "Failed to fetch empresas" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/erp/empresas/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tenantId = (req as any).user?.tenantId;
|
||||
if (!tenantId) return res.status(401).json({ error: "Tenant not identified" });
|
||||
const id = parseInt(req.params.id);
|
||||
const [empresa] = await db.select().from(tenantEmpresas)
|
||||
.where(and(eq(tenantEmpresas.id, id), eq(tenantEmpresas.tenantId, tenantId)));
|
||||
if (!empresa) return res.status(404).json({ error: "Empresa not found" });
|
||||
res.json(empresa);
|
||||
} catch (error) {
|
||||
console.error("Error fetching empresa:", error);
|
||||
res.status(500).json({ error: "Failed to fetch empresa" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/erp/empresas", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tenantId = (req as any).user?.tenantId;
|
||||
if (!tenantId) return res.status(401).json({ error: "Tenant not identified" });
|
||||
const { razaoSocial, nomeFantasia, cnpj, ie, im, email, phone, tipo, cep, logradouro, numero, complemento, bairro, cidade, uf, codigoIbge, regimeTributario } = req.body;
|
||||
if (!razaoSocial || !cnpj) return res.status(400).json({ error: "razaoSocial and cnpj required" });
|
||||
const [empresa] = await db.insert(tenantEmpresas).values({
|
||||
tenantId,
|
||||
razaoSocial,
|
||||
nomeFantasia: nomeFantasia || null,
|
||||
cnpj,
|
||||
ie: ie || null,
|
||||
im: im || null,
|
||||
email: email || null,
|
||||
phone: phone || null,
|
||||
tipo: tipo || "filial",
|
||||
cep: cep || null,
|
||||
logradouro: logradouro || null,
|
||||
numero: numero || null,
|
||||
complemento: complemento || null,
|
||||
bairro: bairro || null,
|
||||
cidade: cidade || null,
|
||||
uf: uf || null,
|
||||
codigoIbge: codigoIbge || null,
|
||||
regimeTributario: regimeTributario || null,
|
||||
}).returning();
|
||||
res.status(201).json(empresa);
|
||||
} catch (error) {
|
||||
console.error("Error creating empresa:", error);
|
||||
res.status(500).json({ error: "Failed to create empresa" });
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/erp/empresas/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tenantId = (req as any).user?.tenantId;
|
||||
if (!tenantId) return res.status(401).json({ error: "Tenant not identified" });
|
||||
const id = parseInt(req.params.id);
|
||||
const [existing] = await db.select().from(tenantEmpresas)
|
||||
.where(and(eq(tenantEmpresas.id, id), eq(tenantEmpresas.tenantId, tenantId)));
|
||||
if (!existing) return res.status(404).json({ error: "Empresa not found" });
|
||||
const { razaoSocial, nomeFantasia, cnpj, ie, im, email, phone, tipo, status, cep, logradouro, numero, complemento, bairro, cidade, uf, codigoIbge, regimeTributario } = req.body;
|
||||
const [updated] = await db.update(tenantEmpresas)
|
||||
.set({
|
||||
...(razaoSocial !== undefined && { razaoSocial }),
|
||||
...(nomeFantasia !== undefined && { nomeFantasia }),
|
||||
...(cnpj !== undefined && { cnpj }),
|
||||
...(ie !== undefined && { ie }),
|
||||
...(im !== undefined && { im }),
|
||||
...(email !== undefined && { email }),
|
||||
...(phone !== undefined && { phone }),
|
||||
...(tipo !== undefined && { tipo }),
|
||||
...(status !== undefined && { status }),
|
||||
...(cep !== undefined && { cep }),
|
||||
...(logradouro !== undefined && { logradouro }),
|
||||
...(numero !== undefined && { numero }),
|
||||
...(complemento !== undefined && { complemento }),
|
||||
...(bairro !== undefined && { bairro }),
|
||||
...(cidade !== undefined && { cidade }),
|
||||
...(uf !== undefined && { uf }),
|
||||
...(codigoIbge !== undefined && { codigoIbge }),
|
||||
...(regimeTributario !== undefined && { regimeTributario }),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(tenantEmpresas.id, id), eq(tenantEmpresas.tenantId, tenantId)))
|
||||
.returning();
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error("Error updating empresa:", error);
|
||||
res.status(500).json({ error: "Failed to update empresa" });
|
||||
}
|
||||
});
|
||||
}
|
||||
// SOE routes — placeholder
|
||||
|
||||
export function registerSoeRoutes(_app: Express): void {}
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ Responda APENAS com o código, sem explicações adicionais.`;
|
|||
: prompt;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "arcadia-agent",
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
|
|
@ -228,7 +228,7 @@ Use português brasileiro.`;
|
|||
: message;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "arcadia-agent",
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@ import { sql } from "drizzle-orm";
|
|||
|
||||
const router = Router();
|
||||
|
||||
function requireAuth(req: any, res: any, next: any) {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
router.get("/courses", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { category, featured, published } = req.query;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import type { Express, Request, Response } from "express";
|
||||
import { manusService } from "./service";
|
||||
import { db } from "../../db/index";
|
||||
import { manusRuns, manusSteps } from "@shared/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
|
||||
export function registerManusRoutes(app: Express): void {
|
||||
const handleManusRun = async (req: Request, res: Response) => {
|
||||
|
|
@ -64,40 +61,4 @@ export function registerManusRoutes(app: Express): void {
|
|||
res.status(500).json({ error: "Failed to get runs" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/manus/runs/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
const runId = parseInt(req.params.id);
|
||||
manusService.cancelRun(runId); // sinaliza o loop para parar
|
||||
await db.delete(manusSteps).where(eq(manusSteps.runId, runId));
|
||||
await db.delete(manusRuns).where(and(eq(manusRuns.id, runId), eq(manusRuns.userId, req.user!.id)));
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Manus delete run error:", error);
|
||||
res.status(500).json({ error: "Failed to delete run" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/manus/runs", async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
const userRuns = await manusService.getUserRuns(req.user!.id);
|
||||
const runIds = userRuns.map((r: any) => r.id);
|
||||
if (runIds.length > 0) {
|
||||
for (const id of runIds) {
|
||||
await db.delete(manusSteps).where(eq(manusSteps.runId, id));
|
||||
}
|
||||
await db.delete(manusRuns).where(eq(manusRuns.userId, req.user!.id));
|
||||
}
|
||||
res.json({ success: true, deleted: runIds.length });
|
||||
} catch (error) {
|
||||
console.error("Manus delete all runs error:", error);
|
||||
res.status(500).json({ error: "Failed to delete runs" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,75 +12,143 @@ import * as erpnextService from "../erpnext/service";
|
|||
const openai = new OpenAI({
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
timeout: 30000,
|
||||
maxRetries: 3,
|
||||
});
|
||||
|
||||
const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, assistente empresarial inteligente da Arcádia Suite.
|
||||
const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, um assistente empresarial inteligente e proativo.
|
||||
|
||||
IDENTIDADE:
|
||||
- Você é o Manus, IA da Arcádia Suite. Se perguntado: "Sou o Manus, agente IA da Arcádia Suite."
|
||||
- Não revele detalhes técnicos da infraestrutura (modelos, APIs, servidores).
|
||||
|
||||
REGRAS DE COMPORTAMENTO:
|
||||
- Seja PROATIVO: execute análises sem pedir confirmação desnecessária
|
||||
- Para perguntas simples e cálculos: responda DIRETO, sem usar ferramentas
|
||||
- NUNCA adicione SWOT, Canvas, PDCA, frameworks ou análises NÃO solicitados
|
||||
- Para AÇÕES DESTRUTIVAS (deletar, modificar dados): informe o que será feito antes de executar
|
||||
- Máximo de 10 passos por execução
|
||||
- Complete SEMPRE a tarefa e apresente o resultado final
|
||||
|
||||
ANÁLISE DE DADOS (quando usar ferramentas de dados):
|
||||
- Apresente dados em TABELAS MARKDOWN formatadas (| Coluna | Valor |)
|
||||
- Calcule variações percentuais, identifique tendências
|
||||
- Forneça insights e interpretações, não só números brutos
|
||||
- Ao analisar documentos: extraia dados E forneça interpretação completa
|
||||
|
||||
CRIAÇÃO DE GRÁFICOS:
|
||||
- Use generate_chart para gráficos visuais (NÃO use python_execute para isso)
|
||||
- Tipos: bar (barras), line (linha), pie (pizza), area (área)
|
||||
- Dados: JSON array — [{"name":"2023","valor":100}]
|
||||
- Use múltiplas séries quando fizer sentido
|
||||
|
||||
PESQUISA E CONHECIMENTO:
|
||||
- Para PESQUISA PROFUNDA: use deep_research (busca e sintetiza múltiplas fontes)
|
||||
- Para APRENDER uma URL: use learn_url
|
||||
- Para BUSCAR conhecimento interno: use semantic_search PRIMEIRO, depois deep_research se necessário
|
||||
- Para NAVEGAR em página: use web_browse
|
||||
- NUNCA diga "não consigo acessar" — sempre tente as ferramentas
|
||||
|
||||
MÓDULO BI (ARCÁDIA INSIGHTS):
|
||||
- bi_stats: estatísticas gerais do BI
|
||||
- bi_list_tables: tabelas disponíveis
|
||||
- bi_get_table_columns: colunas de uma tabela
|
||||
- bi_create_dataset: criar consultas SQL
|
||||
- bi_execute_query: executar dataset e obter dados
|
||||
- bi_create_chart: criar gráficos persistentes
|
||||
- bi_create_dashboard: organizar gráficos em painéis
|
||||
|
||||
COMUNICAÇÃO ENTRE AGENTES (A2A):
|
||||
- list_agents: ver agentes disponíveis
|
||||
- call_agent: delegar tarefas a agentes especializados
|
||||
- Para tarefas especializadas (fiscal, jurídico, vendas): verifique agentes registrados
|
||||
Você executa tarefas usando as ferramentas disponíveis.
|
||||
Você opera em ciclos de pensamento-ação:
|
||||
1. PENSAMENTO: Analise a situação e decida o próximo passo
|
||||
2. AÇÃO: Execute uma ferramenta
|
||||
3. OBSERVAÇÃO: Analise o resultado
|
||||
4. Repita até completar a tarefa
|
||||
|
||||
FERRAMENTAS DISPONÍVEIS:
|
||||
${getToolsDescription()}
|
||||
|
||||
FORMATO DE RESPOSTA OBRIGATÓRIO (JSON):
|
||||
{"thought": "raciocínio sobre o próximo passo", "tool": "nome_ferramenta", "tool_input": {"param": "valor"}}
|
||||
REGRAS DE AUTONOMIA:
|
||||
- Para ANÁLISES e CONSULTAS: seja proativo e execute sem pedir confirmação
|
||||
- Para GERAÇÃO DE CÓDIGO: execute o código e apresente o resultado
|
||||
- Se uma ferramenta falhar, tente uma alternativa ou apresente o que conseguiu
|
||||
- NUNCA fique "aguardando resposta" no meio da tarefa - complete sempre
|
||||
- Se não conseguir gerar um gráfico visualmente, forneça os dados em formato de tabela
|
||||
- Para AÇÕES DESTRUTIVAS (deletar, modificar dados críticos): informe o que será feito na resposta final
|
||||
- Sempre complete a tarefa e apresente o resultado ao final
|
||||
- Máximo de 10 passos por execução
|
||||
|
||||
Para concluir:
|
||||
{"thought": "raciocínio final", "tool": "finish", "tool_input": {"answer": "resposta COMPLETA com dados, tabelas, análise e conclusão"}}
|
||||
COMPORTAMENTO IMPORTANTE:
|
||||
- Quando o usuário pedir análise de dados, faça uma análise COMPLETA e PROFISSIONAL
|
||||
- Sempre forneça insights e interpretações, não apenas os números brutos
|
||||
- Calcule variações percentuais, identifique tendências e faça observações relevantes
|
||||
- Apresente dados em TABELAS FORMATADAS usando Markdown quando apropriado
|
||||
|
||||
REGRA CRÍTICA: O campo "answer" deve conter TODO o conteúdo — tabelas, cálculos, insights. NUNCA apenas "relatório gerado".`;
|
||||
FORMATO DE RESPOSTA IDEAL:
|
||||
1. Primeiro, apresente uma TABELA com os dados extraídos (use formato Markdown: | Coluna | Valor |)
|
||||
2. Em seguida, forneça uma ANÁLISE explicativa com insights (variações %, tendências, observações)
|
||||
3. Por fim, gere um GRÁFICO visual usando generate_chart
|
||||
|
||||
CRIAÇÃO DE GRÁFICOS:
|
||||
- Use generate_chart para criar gráficos visuais (NÃO use python_execute)
|
||||
- Tipos disponíveis: bar (barras), line (linha), pie (pizza), area (área)
|
||||
- Formate os dados como JSON array: [{"name":"2023","ativo":10844216,"passivo":10844216}]
|
||||
- Inclua múltiplas séries quando fizer sentido (ex: ativo E passivo no mesmo gráfico)
|
||||
|
||||
EXEMPLO DE RESPOSTA COMPLETA:
|
||||
1. analyze_file -> extrair dados do documento
|
||||
2. generate_chart -> criar gráfico visual
|
||||
3. finish -> apresentar tabela + análise + conclusão
|
||||
|
||||
Sempre calcule e mencione:
|
||||
- Variações percentuais entre períodos
|
||||
- Tendências (crescimento/queda)
|
||||
- Observações sobre equilíbrio contábil quando aplicável
|
||||
|
||||
PESQUISA INTELIGENTE:
|
||||
- Para PESQUISA PROFUNDA sobre um tema: use deep_research (busca, extrai e sintetiza múltiplas fontes)
|
||||
- Para APRENDER conteúdo de uma URL específica: use learn_url
|
||||
- Para BUSCAR no conhecimento já aprendido: use semantic_search PRIMEIRO
|
||||
- Para NAVEGAR e extrair conteúdo de uma página: use web_browse
|
||||
|
||||
ESTRATÉGIA DE PESQUISA (siga esta ordem):
|
||||
1. PRIMEIRO: Consulte semantic_search para ver se já temos informações sobre o assunto
|
||||
2. SE não houver informações: Use deep_research para pesquisar na web e aprender
|
||||
3. SE o usuário fornecer uma URL específica: Use web_browse ou learn_url
|
||||
4. SEMPRE sintetize e apresente uma resposta completa
|
||||
|
||||
REGRAS DE PESQUISA:
|
||||
- Quando o usuário pedir para "pesquisar sobre X", use deep_research
|
||||
- Quando o usuário mencionar URLs, use web_browse para ver ou learn_url para salvar
|
||||
- Seja PROATIVO: se não encontrar na base interna, pesquise na web automaticamente
|
||||
- NUNCA diga "não consigo acessar" - sempre tente as ferramentas disponíveis
|
||||
|
||||
MÓDULO DE BI (ARCÁDIA INSIGHTS):
|
||||
- Use bi_stats para ver estatísticas gerais do BI
|
||||
- Use bi_list_tables para listar tabelas disponíveis no banco
|
||||
- Use bi_get_table_columns para ver colunas de uma tabela
|
||||
- Use bi_create_dataset para criar consultas SQL ou selecionar tabelas
|
||||
- Use bi_execute_query para executar um dataset e obter dados
|
||||
- Use bi_create_chart para criar gráficos persistentes no BI
|
||||
- Use bi_create_dashboard para organizar gráficos em painéis
|
||||
- Os recursos do BI ficam salvos permanentemente no sistema
|
||||
|
||||
COMUNICAÇÃO ENTRE AGENTES (A2A - Agent to Agent):
|
||||
- Use list_agents para ver agentes externos disponíveis
|
||||
- Use register_agent para adicionar um novo agente externo
|
||||
- Use discover_agent para descobrir capacidades de um agente via Agent Card
|
||||
- Use call_agent para enviar mensagens e delegar tarefas a outros agentes
|
||||
- Você pode orquestrar múltiplos agentes para tarefas complexas
|
||||
- Agentes podem ter especializações (fiscal, jurídico, vendas, etc.)
|
||||
|
||||
ESTRATÉGIA DE ORQUESTRAÇÃO:
|
||||
- Para tarefas especializadas: verifique se há um agente especialista registrado
|
||||
- Para tarefas complexas: divida em subtarefas e delegue para agentes apropriados
|
||||
- Sempre sintetize as respostas dos agentes antes de apresentar ao usuário
|
||||
|
||||
Responda SEMPRE em formato JSON:
|
||||
{
|
||||
"thought": "Seu raciocínio sobre o próximo passo",
|
||||
"tool": "nome_da_ferramenta",
|
||||
"tool_input": { "param1": "valor1" }
|
||||
}
|
||||
|
||||
Quando concluir, use:
|
||||
{
|
||||
"thought": "Raciocínio final",
|
||||
"tool": "finish",
|
||||
"tool_input": { "answer": "Resposta final COMPLETA com TODOS os dados e análise" }
|
||||
}
|
||||
|
||||
REGRA CRÍTICA PARA RESPOSTA FINAL:
|
||||
- A resposta no campo "answer" deve conter TODO o conteúdo da análise
|
||||
- NUNCA diga apenas "relatório gerado com sucesso" - inclua o conteúdo completo
|
||||
- Inclua: tabelas de dados, cálculos, variações percentuais, insights e conclusões
|
||||
- O usuário quer ver a análise completa, não apenas uma confirmação
|
||||
- Se analisou um documento, inclua os dados extraídos E sua interpretação`;
|
||||
|
||||
class ManusService extends EventEmitter {
|
||||
private cancelledRuns = new Set<number>();
|
||||
private pendingApprovals: Map<string, { tool: string; input: Record<string, any> }> = new Map();
|
||||
|
||||
cancelRun(runId: number) {
|
||||
this.cancelledRuns.add(runId);
|
||||
setTimeout(() => this.cancelledRuns.delete(runId), 60000); // limpa após 1 min
|
||||
}
|
||||
private async executeTool(tool: string, input: Record<string, any>, userId: string): Promise<ToolResult> {
|
||||
// Dangerous tools require explicit user approval via ask_human first
|
||||
const DANGEROUS_TOOLS = new Set(["shell", "write_file", "python_execute"]);
|
||||
if (DANGEROUS_TOOLS.has(tool)) {
|
||||
const approvalKey = `${userId}:${tool}:${JSON.stringify(input)}`;
|
||||
if (!this.pendingApprovals.has(approvalKey)) {
|
||||
this.pendingApprovals.set(approvalKey, { tool, input });
|
||||
const preview = tool === "shell" ? input.command
|
||||
: tool === "write_file" ? `Escrever em: ${input.path}`
|
||||
: `Executar código Python (${String(input.code || "").substring(0, 80)}...)`;
|
||||
return {
|
||||
success: false,
|
||||
output: `[APROVAÇÃO NECESSÁRIA] Esta ação requer confirmação: ${preview}. Use ask_human para solicitar aprovação antes de prosseguir.`,
|
||||
error: "requires_approval"
|
||||
};
|
||||
}
|
||||
this.pendingApprovals.delete(approvalKey);
|
||||
}
|
||||
|
||||
async executeTool(tool: string, input: Record<string, any>, userId: string): Promise<ToolResult> {
|
||||
try {
|
||||
switch (tool) {
|
||||
case "web_search":
|
||||
|
|
@ -267,6 +335,13 @@ class ManusService extends EventEmitter {
|
|||
return this.toolRetailStats(input.period, input.storeId);
|
||||
case "retail_report":
|
||||
return this.toolRetailReport(input.type, input.dateFrom, input.dateTo, input.storeId);
|
||||
// ========== AUTOMAÇÃO + XOS + INBOX ==========
|
||||
case "automation_trigger":
|
||||
return this.toolAutomationTrigger(input.automation_id, input.event_type, input.payload, input.tenant_id, userId);
|
||||
case "xos_action":
|
||||
return this.toolXosAction(input.action, input.data, userId);
|
||||
case "inbox_action":
|
||||
return this.toolInboxAction(input.action, input.conversation_id, input.data, userId);
|
||||
case "finish":
|
||||
let finishOutput = input.answer || "";
|
||||
if (input.chart) {
|
||||
|
|
@ -2184,19 +2259,6 @@ class ManusService extends EventEmitter {
|
|||
return { runId: run.id };
|
||||
}
|
||||
|
||||
async runSync(userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>): Promise<string> {
|
||||
const [run] = await db.insert(manusRuns).values({
|
||||
userId,
|
||||
prompt,
|
||||
status: "running"
|
||||
}).returning();
|
||||
|
||||
await this.executeAgentLoop(run.id, userId, prompt, attachedFiles);
|
||||
|
||||
const [completed] = await db.select().from(manusRuns).where(eq(manusRuns.id, run.id));
|
||||
return completed?.result || "Não foi possível processar a solicitação.";
|
||||
}
|
||||
|
||||
private async executeAgentLoop(runId: number, userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>, conversationHistory?: Array<{role: string; content: string}>) {
|
||||
let userPrompt = prompt;
|
||||
if (attachedFiles && attachedFiles.length > 0) {
|
||||
|
|
@ -2206,7 +2268,7 @@ class ManusService extends EventEmitter {
|
|||
|
||||
// Build messages with conversation history for context
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: SYSTEM_PROMPT + `\n\nDATA/HORA ATUAL: ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo', dateStyle: 'full', timeStyle: 'short' })}` }
|
||||
{ role: "system", content: SYSTEM_PROMPT }
|
||||
];
|
||||
|
||||
// Add conversation history if available
|
||||
|
|
@ -2230,25 +2292,13 @@ class ManusService extends EventEmitter {
|
|||
while (step < maxSteps && !finished) {
|
||||
step++;
|
||||
|
||||
// Verificar cancelamento antes de cada step
|
||||
if (this.cancelledRuns.has(runId)) {
|
||||
this.cancelledRuns.delete(runId);
|
||||
await db.update(manusRuns).set({ status: "stopped", completedAt: new Date() }).where(eq(manusRuns.id, runId));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const STEP_TIMEOUT_MS = 60000; // 60s por step
|
||||
const responsePromise = openai.chat.completions.create({
|
||||
model: "arcadia-agent",
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages,
|
||||
temperature: 0.2,
|
||||
max_tokens: 1500,
|
||||
max_tokens: 4000,
|
||||
});
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Step timeout (60s)")), STEP_TIMEOUT_MS)
|
||||
);
|
||||
const response = await Promise.race([responsePromise, timeoutPromise]);
|
||||
|
||||
const content = response.choices[0]?.message?.content || "";
|
||||
messages.push({ role: "assistant", content });
|
||||
|
|
@ -3828,6 +3878,253 @@ class ManusService extends EventEmitter {
|
|||
return { success: false, output: "", error: `Erro ao gerar relatório: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TOOL: automation_trigger
|
||||
// ============================================================
|
||||
private async toolAutomationTrigger(
|
||||
automationId: number | undefined,
|
||||
eventType: string | undefined,
|
||||
payload: string | undefined,
|
||||
tenantId: number | undefined,
|
||||
userId: string
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const parsedPayload = payload ? (typeof payload === 'string' ? JSON.parse(payload) : payload) : {};
|
||||
|
||||
if (automationId) {
|
||||
// Direct automation execution via AutomationService
|
||||
const { automationService } = await import("../automations/service");
|
||||
const result = await automationService.runAutomation(automationId, userId, { ...parsedPayload, triggered_by: "manus" });
|
||||
return {
|
||||
success: result.success,
|
||||
output: `Automação #${automationId} ${result.success ? 'executada com sucesso' : 'falhou'}. Log ID: ${result.logId}. ${result.result || result.error || ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (eventType) {
|
||||
// Emit event to automation engine via HTTP
|
||||
const engineHost = process.env.AUTOMATION_ENGINE_HOST || "localhost";
|
||||
const enginePort = process.env.AUTOMATION_ENGINE_PORT || "8005";
|
||||
const response = await fetch(`http://${engineHost}:${enginePort}/xos/trigger`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event_type: eventType, tenant_id: tenantId, payload: parsedPayload }),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Engine retornou ${response.status}`);
|
||||
const result: any = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
output: `Evento '${eventType}' emitido. ${result.triggered_handlers?.length || 0} handlers ativados.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { success: false, output: "", error: "Informe automation_id ou event_type" };
|
||||
} catch (error: any) {
|
||||
return { success: false, output: "", error: `Erro ao disparar automação: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TOOL: xos_action
|
||||
// ============================================================
|
||||
private async toolXosAction(action: string, data: string | object, userId: string): Promise<ToolResult> {
|
||||
try {
|
||||
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
||||
|
||||
switch (action) {
|
||||
case "create_contact": {
|
||||
const result = await db.execute(sql`
|
||||
INSERT INTO xos_contacts (name, email, phone, whatsapp, type, company, position, source, tags, notes)
|
||||
VALUES (${parsed.name}, ${parsed.email || null}, ${parsed.phone || null}, ${parsed.whatsapp || null},
|
||||
${parsed.type || 'lead'}, ${parsed.company || null}, ${parsed.position || null},
|
||||
${parsed.source || 'manus'}, ${parsed.tags || null}, ${parsed.notes || null})
|
||||
RETURNING id, name, email, type
|
||||
`);
|
||||
const contact = (result.rows || result)[0] as any;
|
||||
return { success: true, output: `Contato criado: ${contact.name} (ID: ${contact.id}, tipo: ${contact.type})` };
|
||||
}
|
||||
|
||||
case "update_contact": {
|
||||
const { id, ...fields } = parsed;
|
||||
if (!id) return { success: false, output: "", error: "id obrigatório para update_contact" };
|
||||
const sets = Object.entries(fields).map(([k, v]) => `${k} = '${v}'`).join(", ");
|
||||
await db.execute(sql`UPDATE xos_contacts SET ${sql.raw(sets)}, updated_at = NOW() WHERE id = ${id}`);
|
||||
return { success: true, output: `Contato #${id} atualizado com sucesso.` };
|
||||
}
|
||||
|
||||
case "create_deal": {
|
||||
const result = await db.execute(sql`
|
||||
INSERT INTO xos_deals (title, pipeline_id, stage_id, contact_id, company_id, value, currency, assigned_to, expected_close_date, notes)
|
||||
VALUES (${parsed.title}, ${parsed.pipeline_id}, ${parsed.stage_id}, ${parsed.contact_id || null},
|
||||
${parsed.company_id || null}, ${parsed.value || null}, ${parsed.currency || 'BRL'},
|
||||
${parsed.assigned_to || null}, ${parsed.expected_close_date || null}, ${parsed.notes || null})
|
||||
RETURNING id, title, value
|
||||
`);
|
||||
const deal = (result.rows || result)[0] as any;
|
||||
return { success: true, output: `Deal criado: "${deal.title}" (ID: ${deal.id}, valor: ${deal.value})` };
|
||||
}
|
||||
|
||||
case "move_deal_stage": {
|
||||
const { deal_id, stage_id } = parsed;
|
||||
if (!deal_id || !stage_id) return { success: false, output: "", error: "deal_id e stage_id obrigatórios" };
|
||||
await db.execute(sql`UPDATE xos_deals SET stage_id = ${stage_id}, updated_at = NOW() WHERE id = ${deal_id}`);
|
||||
return { success: true, output: `Deal #${deal_id} movido para estágio #${stage_id}.` };
|
||||
}
|
||||
|
||||
case "create_ticket": {
|
||||
const result = await db.execute(sql`
|
||||
INSERT INTO xos_tickets (title, description, contact_id, conversation_id, priority, status, category, assigned_to)
|
||||
VALUES (${parsed.title}, ${parsed.description || null}, ${parsed.contact_id || null},
|
||||
${parsed.conversation_id || null}, ${parsed.priority || 'medium'}, 'open',
|
||||
${parsed.category || null}, ${parsed.assigned_to || null})
|
||||
RETURNING id, title, priority
|
||||
`);
|
||||
const ticket = (result.rows || result)[0] as any;
|
||||
return { success: true, output: `Ticket criado: "${ticket.title}" (ID: ${ticket.id}, prioridade: ${ticket.priority})` };
|
||||
}
|
||||
|
||||
case "assign_agent": {
|
||||
const { conversation_id, agent_id } = parsed;
|
||||
if (!conversation_id) return { success: false, output: "", error: "conversation_id obrigatório" };
|
||||
await db.execute(sql`UPDATE xos_conversations SET assigned_to = ${agent_id}, updated_at = NOW() WHERE id = ${conversation_id}`);
|
||||
return { success: true, output: `Agente #${agent_id} atribuído à conversa #${conversation_id}.` };
|
||||
}
|
||||
|
||||
case "create_task": {
|
||||
const result = await db.execute(sql`
|
||||
INSERT INTO xos_activities (contact_id, deal_id, type, title, description, due_date, assigned_to, status)
|
||||
VALUES (${parsed.contact_id || null}, ${parsed.deal_id || null}, 'task',
|
||||
${parsed.title}, ${parsed.description || null},
|
||||
${parsed.due_date || null}, ${parsed.assigned_to || null}, 'pending')
|
||||
RETURNING id, title
|
||||
`);
|
||||
const task = (result.rows || result)[0] as any;
|
||||
return { success: true, output: `Tarefa criada: "${task.title}" (ID: ${task.id})` };
|
||||
}
|
||||
|
||||
case "create_activity": {
|
||||
const result = await db.execute(sql`
|
||||
INSERT INTO xos_activities (contact_id, deal_id, type, title, description, scheduled_at, assigned_to)
|
||||
VALUES (${parsed.contact_id || null}, ${parsed.deal_id || null}, ${parsed.type || 'note'},
|
||||
${parsed.title}, ${parsed.description || null},
|
||||
${parsed.scheduled_at || null}, ${parsed.assigned_to || null})
|
||||
RETURNING id, title, type
|
||||
`);
|
||||
const act = (result.rows || result)[0] as any;
|
||||
return { success: true, output: `Atividade criada: "${act.title}" (tipo: ${act.type}, ID: ${act.id})` };
|
||||
}
|
||||
|
||||
case "create_note": {
|
||||
const result = await db.execute(sql`
|
||||
INSERT INTO xos_internal_notes (conversation_id, content, created_by, is_pinned)
|
||||
VALUES (${parsed.conversation_id || null}, ${parsed.content}, ${userId}, ${parsed.is_pinned || false})
|
||||
RETURNING id
|
||||
`);
|
||||
const note = (result.rows || result)[0] as any;
|
||||
return { success: true, output: `Nota interna criada (ID: ${note.id})` };
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, output: "", error: `Ação XOS desconhecida: ${action}. Use: create_contact, update_contact, create_deal, move_deal_stage, create_ticket, assign_agent, create_task, create_activity, create_note` };
|
||||
}
|
||||
} catch (error: any) {
|
||||
return { success: false, output: "", error: `Erro na ação XOS '${action}': ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TOOL: inbox_action
|
||||
// ============================================================
|
||||
private async toolInboxAction(
|
||||
action: string,
|
||||
conversationId: number | undefined,
|
||||
data: string | object | undefined,
|
||||
userId: string
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const parsed = data ? (typeof data === 'string' ? JSON.parse(data) : data) : {} as any;
|
||||
|
||||
switch (action) {
|
||||
case "close_conversation": {
|
||||
if (!conversationId) return { success: false, output: "", error: "conversation_id obrigatório" };
|
||||
await db.execute(sql`
|
||||
UPDATE xos_conversations SET status = 'closed', closed_at = NOW(), updated_at = NOW()
|
||||
WHERE id = ${conversationId}
|
||||
`);
|
||||
return { success: true, output: `Conversa #${conversationId} fechada.` };
|
||||
}
|
||||
|
||||
case "transfer_conversation": {
|
||||
if (!conversationId) return { success: false, output: "", error: "conversation_id obrigatório" };
|
||||
const { queue_id, agent_id } = parsed;
|
||||
await db.execute(sql`
|
||||
UPDATE xos_conversations SET
|
||||
queue_id = COALESCE(${queue_id || null}, queue_id),
|
||||
assigned_to = COALESCE(${agent_id || null}, assigned_to),
|
||||
updated_at = NOW()
|
||||
WHERE id = ${conversationId}
|
||||
`);
|
||||
return { success: true, output: `Conversa #${conversationId} transferida para fila #${queue_id || 'N/A'} / agente #${agent_id || 'N/A'}.` };
|
||||
}
|
||||
|
||||
case "send_message": {
|
||||
if (!conversationId) return { success: false, output: "", error: "conversation_id obrigatório" };
|
||||
const { content, content_type } = parsed;
|
||||
if (!content) return { success: false, output: "", error: "content obrigatório" };
|
||||
await db.execute(sql`
|
||||
INSERT INTO xos_messages (conversation_id, direction, sender_type, sender_name, content, content_type)
|
||||
VALUES (${conversationId}, 'outbound', 'agent', 'Manus IA', ${content}, ${content_type || 'text'})
|
||||
`);
|
||||
await db.execute(sql`
|
||||
UPDATE xos_conversations SET last_message = ${content}, updated_at = NOW() WHERE id = ${conversationId}
|
||||
`);
|
||||
return { success: true, output: `Mensagem enviada na conversa #${conversationId}: "${content.substring(0, 100)}"` };
|
||||
}
|
||||
|
||||
case "add_label": {
|
||||
if (!conversationId) return { success: false, output: "", error: "conversation_id obrigatório" };
|
||||
const { label } = parsed;
|
||||
await db.execute(sql`
|
||||
UPDATE xos_conversations SET
|
||||
tags = COALESCE(tags, '') || ${label ? ',' + label : ''},
|
||||
updated_at = NOW()
|
||||
WHERE id = ${conversationId}
|
||||
`);
|
||||
return { success: true, output: `Etiqueta '${label}' adicionada à conversa #${conversationId}.` };
|
||||
}
|
||||
|
||||
case "resolve_ticket": {
|
||||
const { ticket_id, resolution } = parsed;
|
||||
if (!ticket_id) return { success: false, output: "", error: "ticket_id obrigatório" };
|
||||
await db.execute(sql`
|
||||
UPDATE xos_tickets SET status = 'resolved', resolution = ${resolution || null},
|
||||
resolved_at = NOW(), updated_at = NOW()
|
||||
WHERE id = ${ticket_id}
|
||||
`);
|
||||
return { success: true, output: `Ticket #${ticket_id} resolvido.` };
|
||||
}
|
||||
|
||||
case "escalate_ticket": {
|
||||
const { ticket_id, priority, reason } = parsed;
|
||||
if (!ticket_id) return { success: false, output: "", error: "ticket_id obrigatório" };
|
||||
await db.execute(sql`
|
||||
UPDATE xos_tickets SET priority = ${priority || 'urgent'},
|
||||
notes = CONCAT(COALESCE(notes, ''), ' [Escalado por Manus: ', ${reason || 'sem motivo'}, ']'),
|
||||
updated_at = NOW()
|
||||
WHERE id = ${ticket_id}
|
||||
`);
|
||||
return { success: true, output: `Ticket #${ticket_id} escalado para prioridade ${priority || 'urgent'}.` };
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, output: "", error: `Ação de inbox desconhecida: ${action}. Use: close_conversation, transfer_conversation, send_message, add_label, resolve_ticket, escalate_ticket` };
|
||||
}
|
||||
} catch (error: any) {
|
||||
return { success: false, output: "", error: `Erro na ação de inbox '${action}': ${error.message}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const manusService = new ManusService();
|
||||
|
|
|
|||
|
|
@ -733,6 +733,33 @@ export const MANUS_TOOLS: ManusToolDef[] = [
|
|||
dateTo: { type: "string", description: "Data final (YYYY-MM-DD)", required: false },
|
||||
storeId: { type: "number", description: "ID da loja para filtrar", required: false }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "automation_trigger",
|
||||
description: "Dispara uma automação existente ou emite um evento no barramento. Use para encadear automações, disparar workflows CRM, ou emitir eventos personalizados que ativam outras automações.",
|
||||
parameters: {
|
||||
automation_id: { type: "number", description: "ID da automação a executar (opcional se usar event_type)", required: false },
|
||||
event_type: { type: "string", description: "Tipo de evento a emitir no barramento: crm.contact.created, crm.deal.stage_changed, crm.ticket.created, crm.message.received, manual.trigger, system.event, etc.", required: false },
|
||||
payload: { type: "string", description: "Dados adicionais em JSON para passar à automação ou ao evento (ex: '{\"contact_id\": 42, \"stage\": \"qualified\"}')", required: false },
|
||||
tenant_id: { type: "number", description: "ID do tenant para automações multi-tenant (xos_automations)", required: false }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "xos_action",
|
||||
description: "Executa ações no XOS CRM: criar/atualizar contatos, mover deals no pipeline, criar tickets, atribuir agentes, criar tarefas, enviar campanhas. Use para automatizar operações de CRM via IA.",
|
||||
parameters: {
|
||||
action: { type: "string", description: "Ação a executar: create_contact, update_contact, create_deal, move_deal_stage, create_ticket, assign_agent, create_task, create_activity, send_campaign, create_note", required: true },
|
||||
data: { type: "string", description: "Dados para a ação em JSON. Ex: create_contact: '{\"name\":\"João\",\"email\":\"joao@ex.com\",\"type\":\"lead\"}'; move_deal_stage: '{\"deal_id\":5,\"stage_id\":3}'; create_ticket: '{\"title\":\"Bug\",\"contact_id\":2,\"priority\":\"high\"}'", required: true }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "inbox_action",
|
||||
description: "Executa ações na Central de Atendimento: fechar/transferir conversas, enviar mensagens automáticas, atribuir agentes, adicionar etiquetas, criar protocolos, alterar status de tickets de suporte.",
|
||||
parameters: {
|
||||
action: { type: "string", description: "Ação: close_conversation, transfer_conversation, send_message, assign_agent, add_label, create_protocol, resolve_ticket, escalate_ticket, send_csat", required: true },
|
||||
conversation_id: { type: "number", description: "ID da conversa (obrigatório para ações em conversas existentes)", required: false },
|
||||
data: { type: "string", description: "Dados adicionais em JSON. Ex: transfer: '{\"queue_id\":2}'; send_message: '{\"content\":\"Olá!\",\"type\":\"text\"}'; assign_agent: '{\"agent_id\":5}'", required: false }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,52 @@ import { eq, desc, and, sql } from "drizzle-orm";
|
|||
|
||||
const router = Router();
|
||||
|
||||
function requireAuth(req: any, res: any, next: any) {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// My apps: returns active subscriptions for the current user's tenant
|
||||
// Public modules (isCore=true) are always included
|
||||
router.get("/my-apps", requireAuth, async (req: any, res: Response) => {
|
||||
try {
|
||||
const tenantId = req.user?.tenantId;
|
||||
|
||||
// Core modules always available
|
||||
const coreModules = await db
|
||||
.select({ code: marketplaceModules.code, route: marketplaceModules.route })
|
||||
.from(marketplaceModules)
|
||||
.where(and(eq(marketplaceModules.isCore, true), eq(marketplaceModules.isActive, true)));
|
||||
|
||||
// Subscribed modules for this tenant
|
||||
const subscribed = tenantId
|
||||
? await db
|
||||
.select({ code: marketplaceModules.code, route: marketplaceModules.route })
|
||||
.from(moduleSubscriptions)
|
||||
.innerJoin(marketplaceModules, eq(moduleSubscriptions.moduleId, marketplaceModules.id))
|
||||
.where(
|
||||
and(
|
||||
eq(moduleSubscriptions.tenantId, tenantId),
|
||||
eq(moduleSubscriptions.status, "active"),
|
||||
eq(marketplaceModules.isActive, true)
|
||||
)
|
||||
)
|
||||
: [];
|
||||
|
||||
const allCodes = new Set([
|
||||
...coreModules.map((m) => m.code),
|
||||
...subscribed.map((m) => m.code),
|
||||
]);
|
||||
|
||||
res.json({ subscribedCodes: [...allCodes], tenantId: tenantId || null });
|
||||
} catch (error) {
|
||||
console.error("Error fetching my-apps:", error);
|
||||
res.status(500).json({ error: "Failed to fetch subscribed apps" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/modules", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const modules = await db
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ import { metasetClient } from "./client";
|
|||
const METASET_HOST = process.env.METABASE_HOST || "localhost";
|
||||
const METASET_PORT = parseInt(process.env.METABASE_PORT || "8088", 10);
|
||||
const METASET_URL = `http://${METASET_HOST}:${METASET_PORT}`;
|
||||
const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL || "admin@arcadia.app";
|
||||
const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD || "Arcadia2026!BI";
|
||||
const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL;
|
||||
const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD;
|
||||
|
||||
export function registerMetaSetRoutes(app: Express): void {
|
||||
|
||||
app.get("/api/bi/metaset/autologin", async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||
return res.status(503).json({ error: "MetaSet credentials not configured (METASET_ADMIN_EMAIL / METASET_ADMIN_PASSWORD)" });
|
||||
}
|
||||
|
||||
const sessionResp = await fetch(`${METASET_URL}/api/session`, {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -90,8 +90,6 @@ router.post('/upload', upload.single('file'), async (req, res) => {
|
|||
if (fileName.endsWith('.zip')) sourceType = 'mongodb';
|
||||
else if (fileName.endsWith('.json')) sourceType = 'json';
|
||||
else if (fileName.endsWith('.csv')) sourceType = 'csv';
|
||||
else if (fileName.endsWith('.rar')) sourceType = 'sql-rar';
|
||||
else if (fileName.endsWith('.sql.gz') || fileName.endsWith('.sql')) sourceType = 'sql';
|
||||
|
||||
const [job] = await db.insert(migrationJobs).values({
|
||||
name: name || `Migração ${new Date().toLocaleDateString('pt-BR')}`,
|
||||
|
|
@ -150,87 +148,6 @@ router.post('/upload', upload.single('file'), async (req, res) => {
|
|||
});
|
||||
}
|
||||
}
|
||||
} else if (sourceType === 'sql-rar') {
|
||||
const extractDir = path.join(UPLOAD_DIR, `job-${job.id}`);
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
|
||||
await db.update(migrationJobs)
|
||||
.set({ status: 'analyzing' })
|
||||
.where(eq(migrationJobs.id, job.id));
|
||||
|
||||
await execAsync(`7z x "${filePath}" -o"${extractDir}" -y`);
|
||||
|
||||
const allFiles = fs.readdirSync(extractDir);
|
||||
const sqlGzFile = allFiles.find(f => f.endsWith('.sql.gz'));
|
||||
const sqlFile = allFiles.find(f => f.endsWith('.sql'));
|
||||
|
||||
let finalSqlPath: string;
|
||||
if (sqlGzFile) {
|
||||
const gzPath = path.join(extractDir, sqlGzFile);
|
||||
finalSqlPath = gzPath.replace(/\.gz$/, '');
|
||||
await execAsync(`gunzip -f "${gzPath}"`);
|
||||
} else if (sqlFile) {
|
||||
finalSqlPath = path.join(extractDir, sqlFile);
|
||||
} else {
|
||||
throw new Error('Nenhum arquivo .sql ou .sql.gz encontrado dentro do RAR');
|
||||
}
|
||||
|
||||
const sqlContent = fs.readFileSync(finalSqlPath, 'utf8');
|
||||
const tableMatches = sqlContent.match(/CREATE TABLE\s+`?(\w+)`?/gi) || [];
|
||||
const tables = tableMatches.map(m => m.replace(/CREATE TABLE\s+`?/i, '').replace(/`/g, '').trim());
|
||||
const lineCount = sqlContent.split('\n').length;
|
||||
|
||||
await db.update(migrationJobs)
|
||||
.set({
|
||||
status: 'mapping',
|
||||
totalRecords: lineCount,
|
||||
analysisResult: {
|
||||
type: 'sql',
|
||||
sqlPath: finalSqlPath,
|
||||
tables,
|
||||
tableCount: tables.length,
|
||||
lineCount,
|
||||
fileSizeBytes: fs.statSync(finalSqlPath).size
|
||||
},
|
||||
importConfig: { sqlPath: finalSqlPath }
|
||||
})
|
||||
.where(eq(migrationJobs.id, job.id));
|
||||
} else if (sourceType === 'sql') {
|
||||
const extractDir = path.join(UPLOAD_DIR, `job-${job.id}`);
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
|
||||
await db.update(migrationJobs)
|
||||
.set({ status: 'analyzing' })
|
||||
.where(eq(migrationJobs.id, job.id));
|
||||
|
||||
let finalSqlPath: string;
|
||||
if (fileName.endsWith('.sql.gz')) {
|
||||
finalSqlPath = path.join(extractDir, fileName.replace(/\.gz$/, ''));
|
||||
await execAsync(`gunzip -c "${filePath}" > "${finalSqlPath}"`);
|
||||
} else {
|
||||
finalSqlPath = filePath;
|
||||
}
|
||||
|
||||
const sqlContent = fs.readFileSync(finalSqlPath, 'utf8');
|
||||
const tableMatches = sqlContent.match(/CREATE TABLE\s+`?(\w+)`?/gi) || [];
|
||||
const tables = tableMatches.map(m => m.replace(/CREATE TABLE\s+`?/i, '').replace(/`/g, '').trim());
|
||||
const lineCount = sqlContent.split('\n').length;
|
||||
|
||||
await db.update(migrationJobs)
|
||||
.set({
|
||||
status: 'mapping',
|
||||
totalRecords: lineCount,
|
||||
analysisResult: {
|
||||
type: 'sql',
|
||||
sqlPath: finalSqlPath,
|
||||
tables,
|
||||
tableCount: tables.length,
|
||||
lineCount,
|
||||
fileSizeBytes: fs.statSync(finalSqlPath).size
|
||||
},
|
||||
importConfig: { sqlPath: finalSqlPath }
|
||||
})
|
||||
.where(eq(migrationJobs.id, job.id));
|
||||
}
|
||||
|
||||
const [updatedJob] = await db.select().from(migrationJobs).where(eq(migrationJobs.id, job.id));
|
||||
|
|
|
|||
|
|
@ -1,80 +1,8 @@
|
|||
/**
|
||||
* Arcádia Suite - Module Routes Auto-Loader
|
||||
*
|
||||
* Carrega automaticamente todas as rotas de módulos em server/modules/.
|
||||
* Cada arquivo exporta um Router do Express, montado em /api/modules/{nome}.
|
||||
*
|
||||
* Arquivos que começam com _ são ignorados (_template.ts, _utils.ts).
|
||||
* O loader é chamado pelo routes.ts central.
|
||||
*/
|
||||
|
||||
import type { Express } from "express";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import skillsRouter from "../skills/routes";
|
||||
import openclawRouter from "./openclaw/routes";
|
||||
|
||||
function getDirname(): string {
|
||||
try {
|
||||
if (typeof __dirname !== "undefined") return __dirname;
|
||||
} catch {}
|
||||
try {
|
||||
const currentFile = fileURLToPath(import.meta.url);
|
||||
return path.dirname(currentFile);
|
||||
} catch {}
|
||||
return path.resolve("server/modules");
|
||||
}
|
||||
|
||||
const currentDir = getDirname();
|
||||
|
||||
export async function loadModuleRoutes(app: Express): Promise<string[]> {
|
||||
const modulesDir = path.resolve(currentDir);
|
||||
const loaded: string[] = [];
|
||||
|
||||
if (!fs.existsSync(modulesDir)) {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(modulesDir);
|
||||
const moduleFiles = files.filter(
|
||||
f => (f.endsWith(".ts") || f.endsWith(".js")) &&
|
||||
!f.startsWith("_") &&
|
||||
f !== "loader.ts" &&
|
||||
f !== "loader.js" &&
|
||||
f !== "migrator.ts" &&
|
||||
f !== "migrator.js"
|
||||
);
|
||||
|
||||
for (const file of moduleFiles) {
|
||||
const moduleName = file.replace(/\.(ts|js)$/, "");
|
||||
try {
|
||||
const modulePath = path.join(modulesDir, file);
|
||||
const mod = await import(modulePath);
|
||||
const router = mod.default || mod.router;
|
||||
|
||||
if (router) {
|
||||
app.use(`/api/modules/${moduleName}`, router);
|
||||
loaded.push(moduleName);
|
||||
console.log(`[ModuleLoader] Módulo carregado: /api/modules/${moduleName}`);
|
||||
} else {
|
||||
console.warn(`[ModuleLoader] Módulo ${moduleName} não exporta router default`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`[ModuleLoader] Erro ao carregar módulo ${moduleName}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (loaded.length > 0) {
|
||||
console.log(`[ModuleLoader] ${loaded.length} módulos carregados: ${loaded.join(", ")}`);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
export function getLoadedModules(): string[] {
|
||||
const modulesDir = path.resolve(currentDir);
|
||||
if (!fs.existsSync(modulesDir)) return [];
|
||||
|
||||
return fs.readdirSync(modulesDir)
|
||||
.filter(f => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith("_") && f !== "loader.ts" && f !== "loader.js" && f !== "migrator.ts" && f !== "migrator.js")
|
||||
.map(f => f.replace(/\.(ts|js)$/, ""));
|
||||
export async function loadModuleRoutes(app: Express): Promise<void> {
|
||||
app.use("/api/skills", skillsRouter);
|
||||
app.use("/api/openclaw", openclawRouter);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit bd7ec41b51dd89f4c5551e3d27917b41af4a3793
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
/**
|
||||
* OpenClawEngine.ts
|
||||
*
|
||||
* Orquestração central de OpenClaw
|
||||
* Coordena detecção de padrões, geração de skills e sugestões
|
||||
*
|
||||
* Fase 4: OpenClaw Embutido
|
||||
*/
|
||||
|
||||
import { PatternDetector, getPatternDetector, DetectedPattern } from "./PatternDetector";
|
||||
import { SkillEmergence } from "./SkillEmergence";
|
||||
import { EventEmitter } from "events";
|
||||
import { Server as SocketIOServer } from "socket.io";
|
||||
|
||||
interface SkillSuggestion {
|
||||
id: string;
|
||||
pattern: DetectedPattern;
|
||||
suggested_skill_name: string;
|
||||
suggested_description: string;
|
||||
estimated_automation: string;
|
||||
confidence: number;
|
||||
created_at: Date;
|
||||
status: "pending" | "accepted" | "rejected";
|
||||
}
|
||||
|
||||
interface OpenClawEngineConfig {
|
||||
pattern_detection: {
|
||||
enabled: boolean;
|
||||
min_occurrences: number;
|
||||
time_window_days: number;
|
||||
confidence_threshold: number;
|
||||
scheduled_check_interval_minutes: number;
|
||||
};
|
||||
suggestions: {
|
||||
enabled: boolean;
|
||||
auto_suggest: boolean;
|
||||
channels: string[];
|
||||
priority_threshold: number;
|
||||
};
|
||||
emergence: {
|
||||
create_as_draft: boolean;
|
||||
auto_publish: boolean;
|
||||
notify_admins: boolean;
|
||||
skill_prefix: string;
|
||||
};
|
||||
logging: {
|
||||
enabled: boolean;
|
||||
store_patterns: boolean;
|
||||
store_in_kg: boolean;
|
||||
audit_trail: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenClawEngine
|
||||
*
|
||||
* Motor central que coordena todo o fluxo de OpenClaw:
|
||||
* Pattern Detection → Suggestions → Skill Generation → Dev Center
|
||||
*/
|
||||
export class OpenClawEngine extends EventEmitter {
|
||||
private patternDetector: PatternDetector;
|
||||
private skillEmergence: SkillEmergence;
|
||||
private config: OpenClawEngineConfig;
|
||||
private socketIO?: SocketIOServer;
|
||||
private checkInterval?: NodeJS.Timer;
|
||||
private suggestions: Map<string, SkillSuggestion> = new Map();
|
||||
|
||||
constructor(config: Partial<OpenClawEngineConfig>, socketIO?: SocketIOServer) {
|
||||
super();
|
||||
|
||||
// Configuração padrão
|
||||
this.config = {
|
||||
pattern_detection: {
|
||||
enabled: true,
|
||||
min_occurrences: 3,
|
||||
time_window_days: 30,
|
||||
confidence_threshold: 0.8,
|
||||
scheduled_check_interval_minutes: 60,
|
||||
...config?.pattern_detection,
|
||||
},
|
||||
suggestions: {
|
||||
enabled: true,
|
||||
auto_suggest: false,
|
||||
channels: ["widget", "chat"],
|
||||
priority_threshold: 0.85,
|
||||
...config?.suggestions,
|
||||
},
|
||||
emergence: {
|
||||
create_as_draft: true,
|
||||
auto_publish: false,
|
||||
notify_admins: true,
|
||||
skill_prefix: "emergent_",
|
||||
...config?.emergence,
|
||||
},
|
||||
logging: {
|
||||
enabled: true,
|
||||
store_patterns: true,
|
||||
store_in_kg: true,
|
||||
audit_trail: true,
|
||||
...config?.logging,
|
||||
},
|
||||
};
|
||||
|
||||
this.socketIO = socketIO;
|
||||
this.patternDetector = getPatternDetector(this.config.pattern_detection);
|
||||
this.skillEmergence = new SkillEmergence();
|
||||
|
||||
// Setup listeners
|
||||
this.setupListeners();
|
||||
|
||||
console.log("[OpenClawEngine] Inicializado com config:", this.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup dos listeners de eventos internos
|
||||
*/
|
||||
private setupListeners(): void {
|
||||
// Listener: quando PatternDetector detecta um padrão
|
||||
this.patternDetector.on("pattern-detected", async (event) => {
|
||||
console.log("[OpenClawEngine] Padrão detectado:", event.pattern);
|
||||
await this.handlePatternDetected(event.pattern);
|
||||
});
|
||||
|
||||
// Listener: quando usuário confirma uma sugestão
|
||||
this.on("user-confirmed-suggestion", async (suggestionId: string) => {
|
||||
console.log("[OpenClawEngine] Usuário confirmou sugestão:", suggestionId);
|
||||
await this.handleUserConfirmation(suggestionId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detectar padrão manualmente
|
||||
*
|
||||
* Pode ser chamado por:
|
||||
* 1. Sistema ao receber uma interação
|
||||
* 2. Scheduler verificando regularmente
|
||||
* 3. API endpoint de teste
|
||||
*/
|
||||
async detectPattern(userId: string, tenantId: string, actionType: string): Promise<DetectedPattern | null> {
|
||||
if (!this.config.pattern_detection.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pattern = await this.patternDetector.detectPattern(userId, tenantId, actionType);
|
||||
|
||||
if (pattern && pattern.confidence >= this.config.suggestions.priority_threshold) {
|
||||
// Padrão de alta prioridade - sugerir imediatamente
|
||||
await this.suggestSkill(pattern);
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler: Padrão foi detectado
|
||||
*
|
||||
* Fluxo:
|
||||
* 1. Validar padrão
|
||||
* 2. Gerar sugestão de skill
|
||||
* 3. Emitir para widget via Socket.IO
|
||||
* 4. Armazenar sugestão
|
||||
*/
|
||||
private async handlePatternDetected(pattern: DetectedPattern): Promise<void> {
|
||||
try {
|
||||
console.log(`[OpenClawEngine] Processando padrão detectado: ${pattern.action_type}`);
|
||||
|
||||
// 1. Gerar sugestão de skill baseado no padrão
|
||||
const suggestion = await this.suggestSkill(pattern);
|
||||
|
||||
if (!suggestion) {
|
||||
console.log("[OpenClawEngine] Não foi possível gerar sugestão para o padrão");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Emitir evento para Socket.IO (chega no widget frontend)
|
||||
if (this.socketIO) {
|
||||
this.socketIO.emit("openclaw:pattern-detected", {
|
||||
suggestion,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
console.log("[OpenClawEngine] Evento emitido ao Socket.IO");
|
||||
}
|
||||
|
||||
// 3. Armazenar sugestão
|
||||
this.suggestions.set(suggestion.id, suggestion);
|
||||
|
||||
// 4. Notificar admin se configurado
|
||||
if (this.config.emergence.notify_admins) {
|
||||
await this.notifyAdmins(pattern, suggestion);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[OpenClawEngine] Erro ao processar padrão detectado:", error);
|
||||
this.emit("error", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar sugestão de skill baseado em um padrão
|
||||
*
|
||||
* Analisa o padrão e cria uma sugestão descritiva
|
||||
* do que a automação faria
|
||||
*/
|
||||
private async suggestSkill(pattern: DetectedPattern): Promise<SkillSuggestion | null> {
|
||||
try {
|
||||
// Gerar nome e descrição da skill baseado no padrão
|
||||
const skillName = this.generateSkillName(pattern);
|
||||
const skillDescription = this.generateSkillDescription(pattern);
|
||||
const estimatedAutomation = this.generateAutomationPreview(pattern);
|
||||
|
||||
const suggestion: SkillSuggestion = {
|
||||
id: `suggest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
pattern,
|
||||
suggested_skill_name: skillName,
|
||||
suggested_description: skillDescription,
|
||||
estimated_automation: estimatedAutomation,
|
||||
confidence: pattern.confidence,
|
||||
created_at: new Date(),
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
console.log(`[OpenClawEngine] Sugestão gerada: ${skillName}`);
|
||||
|
||||
return suggestion;
|
||||
} catch (error) {
|
||||
console.error("[OpenClawEngine] Erro ao gerar sugestão:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar nome da skill baseado no padrão
|
||||
*/
|
||||
private generateSkillName(pattern: DetectedPattern): string {
|
||||
// Simples: capitalizar o tipo de ação
|
||||
// Exemplo: "query_sales_report" → "Sales Report Query"
|
||||
|
||||
const words = pattern.action_type.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1));
|
||||
return `${this.config.emergence.skill_prefix}${words.join(" ")}`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar descrição da skill baseado no padrão
|
||||
*/
|
||||
private generateSkillDescription(pattern: DetectedPattern): string {
|
||||
return `
|
||||
Automação detectada para: ${pattern.action_type}
|
||||
|
||||
Padrão observado:
|
||||
- Ocorrências: ${pattern.occurrences}x nos últimos ${pattern.time_window_days} dias
|
||||
- Frequência: ${pattern.metadata?.frequency_per_week} vezes/semana
|
||||
- Intervalo médio: ${pattern.metadata?.average_interval_hours} horas
|
||||
- Confiança: ${(pattern.confidence * 100).toFixed(1)}%
|
||||
|
||||
Esta skill automatizará essa ação repetitiva para você.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar preview da automação
|
||||
*/
|
||||
private generateAutomationPreview(pattern: DetectedPattern): string {
|
||||
return `
|
||||
# Automação Proposta
|
||||
|
||||
**Trigger:** Quando ocorre ${pattern.action_type}
|
||||
**Frequência:** A cada ${pattern.metadata?.average_interval_hours} horas aproximadamente
|
||||
**Ação:** Executar automaticamente
|
||||
|
||||
Esta skill vai:
|
||||
1. Monitorar quando você executa a ação: ${pattern.action_type}
|
||||
2. Ao detectar o padrão, executar automaticamente
|
||||
3. Salvar tempo e aumentar produtividade
|
||||
|
||||
**Status:** Pronto para criar como DRAFT e publicar após aprovação
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler: Usuário confirmou uma sugestão
|
||||
*
|
||||
* Fluxo:
|
||||
* 1. Validar sugestão
|
||||
* 2. Chamar SkillEmergence para gerar código
|
||||
* 3. Armazenar skill como DRAFT
|
||||
* 4. Notificar Dev Center
|
||||
*/
|
||||
private async handleUserConfirmation(suggestionId: string): Promise<void> {
|
||||
try {
|
||||
const suggestion = this.suggestions.get(suggestionId);
|
||||
|
||||
if (!suggestion) {
|
||||
console.error(`[OpenClawEngine] Sugestão não encontrada: ${suggestionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[OpenClawEngine] Processando confirmação do usuário para: ${suggestion.suggested_skill_name}`);
|
||||
|
||||
// 1. Gerar skill (código) via Blackboard
|
||||
const skillDraft = await this.skillEmergence.generateSkillDraft(suggestion);
|
||||
|
||||
if (!skillDraft) {
|
||||
console.error("[OpenClawEngine] Falha ao gerar skill");
|
||||
this.emit("error", new Error("Failed to generate skill draft"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Atualizar status da sugestão
|
||||
suggestion.status = "accepted";
|
||||
|
||||
// 3. Emitir evento de sucesso
|
||||
this.emit("skill-created", {
|
||||
suggestion,
|
||||
skill: skillDraft,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
// 4. Notificar via Socket.IO (para frontend)
|
||||
if (this.socketIO) {
|
||||
this.socketIO.emit("openclaw:skill-created", {
|
||||
skill: skillDraft,
|
||||
suggestion,
|
||||
message: `Skill "${suggestion.suggested_skill_name}" criada com sucesso! Vá para Dev Center para publicar.`,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("[OpenClawEngine] Skill criada com sucesso:", skillDraft.id);
|
||||
} catch (error) {
|
||||
console.error("[OpenClawEngine] Erro ao processar confirmação:", error);
|
||||
this.emit("error", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notificar admins sobre novo padrão detectado
|
||||
*/
|
||||
private async notifyAdmins(pattern: DetectedPattern, suggestion: SkillSuggestion): Promise<void> {
|
||||
// TODO: Implementar notificação
|
||||
// Opções:
|
||||
// 1. Email para admins
|
||||
// 2. Notificação em dashboard
|
||||
// 3. Alerta no Slack/Discord
|
||||
// 4. Entry em audit log
|
||||
|
||||
console.log(
|
||||
`[OpenClawEngine] Admin notificado: Novo padrão ${pattern.action_type} (confiança: ${pattern.confidence})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar padrões periodicamente (para ser chamado por scheduler)
|
||||
*/
|
||||
async checkForPatternsScheduled(tenantId: string): Promise<void> {
|
||||
if (!this.config.pattern_detection.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[OpenClawEngine] Verificação agendada de padrões para tenant: ${tenantId}`);
|
||||
|
||||
try {
|
||||
await this.patternDetector.checkForNewPatterns(tenantId);
|
||||
} catch (error) {
|
||||
console.error("[OpenClawEngine] Erro na verificação agendada:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iniciar verificação periódica de padrões
|
||||
*/
|
||||
startScheduledChecks(tenantId: string): void {
|
||||
if (this.checkInterval) {
|
||||
console.log("[OpenClawEngine] Verificação agendada já está ativa");
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalMs = this.config.pattern_detection.scheduled_check_interval_minutes * 60 * 1000;
|
||||
|
||||
this.checkInterval = setInterval(() => {
|
||||
this.checkForPatternsScheduled(tenantId);
|
||||
}, intervalMs);
|
||||
|
||||
console.log(
|
||||
`[OpenClawEngine] Verificação agendada iniciada (a cada ${this.config.pattern_detection.scheduled_check_interval_minutes} min)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parar verificação periódica
|
||||
*/
|
||||
stopScheduledChecks(): void {
|
||||
if (this.checkInterval) {
|
||||
clearInterval(this.checkInterval);
|
||||
this.checkInterval = undefined;
|
||||
console.log("[OpenClawEngine] Verificação agendada interrompida");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter sugestões pendentes para um usuário
|
||||
*/
|
||||
getPendingSuggestions(userId?: string): SkillSuggestion[] {
|
||||
const suggestions = Array.from(this.suggestions.values()).filter((s) => s.status === "pending");
|
||||
|
||||
if (userId) {
|
||||
return suggestions.filter((s) => s.pattern.user_id === userId);
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter configuração
|
||||
*/
|
||||
getConfig(): OpenClawEngineConfig {
|
||||
return JSON.parse(JSON.stringify(this.config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualizar configuração
|
||||
*/
|
||||
setConfig(config: Partial<OpenClawEngineConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
console.log("[OpenClawEngine] Configuração atualizada");
|
||||
|
||||
// Atualizar PatternDetector se necessário
|
||||
if (config.pattern_detection) {
|
||||
this.patternDetector.setConfig(config.pattern_detection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exportar instância singleton
|
||||
let engineInstance: OpenClawEngine | null = null;
|
||||
|
||||
export function getOpenClawEngine(
|
||||
config?: Partial<OpenClawEngineConfig>,
|
||||
socketIO?: SocketIOServer
|
||||
): OpenClawEngine {
|
||||
if (!engineInstance) {
|
||||
engineInstance = new OpenClawEngine(config || {}, socketIO);
|
||||
}
|
||||
return engineInstance;
|
||||
}
|
||||
|
||||
export default OpenClawEngine;
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
/**
|
||||
* PatternDetector.ts
|
||||
*
|
||||
* Detecção proativa de padrões de uso no Arcádia
|
||||
* Monitora eventos de usuário e identifica ações repetidas
|
||||
* para sugerir automações/skills emergentes
|
||||
*
|
||||
* Fase 4: OpenClaw Embutido
|
||||
*/
|
||||
|
||||
import { db } from "@/db";
|
||||
import { detectedPatterns, skillSuggestions } from "@/shared/schema";
|
||||
import { eq, and, gte } from "drizzle-orm";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
interface Interaction {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
user_id: string;
|
||||
action_type: string;
|
||||
action_data: Record<string, any>;
|
||||
timestamp: Date;
|
||||
context?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface DetectedPattern {
|
||||
id?: number;
|
||||
tenant_id: string;
|
||||
user_id: string;
|
||||
action_type: string;
|
||||
occurrences: number;
|
||||
confidence: number;
|
||||
time_window_days: number;
|
||||
created_at?: Date;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface PatternDetectorConfig {
|
||||
min_occurrences: number;
|
||||
time_window_days: number;
|
||||
confidence_threshold: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* PatternDetector
|
||||
*
|
||||
* Analisa interações de usuário e detecta padrões
|
||||
* Emite eventos quando um padrão é confirmado
|
||||
*/
|
||||
export class PatternDetector extends EventEmitter {
|
||||
private config: PatternDetectorConfig;
|
||||
private detectedPatterns: Map<string, DetectedPattern> = new Map();
|
||||
|
||||
constructor(config: PatternDetectorConfig) {
|
||||
super();
|
||||
this.config = {
|
||||
min_occurrences: config.min_occurrences ?? 3,
|
||||
time_window_days: config.time_window_days ?? 30,
|
||||
confidence_threshold: config.confidence_threshold ?? 0.8,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detectar padrão a partir de eventos de usuário
|
||||
*
|
||||
* Fluxo:
|
||||
* 1. Buscar interações similares do usuário (últimos N dias)
|
||||
* 2. Agrupar por tipo de ação
|
||||
* 3. Contar ocorrências
|
||||
* 4. Se >= min_occurrences: calcular confiança
|
||||
* 5. Se confiança >= threshold: emitir evento
|
||||
*/
|
||||
async detectPattern(userId: string, tenantId: string, actionType: string): Promise<DetectedPattern | null> {
|
||||
try {
|
||||
// 1. Buscar interações similares
|
||||
const interactions = await this.getRecentInteractions(
|
||||
userId,
|
||||
tenantId,
|
||||
actionType,
|
||||
this.config.time_window_days
|
||||
);
|
||||
|
||||
if (interactions.length < this.config.min_occurrences) {
|
||||
return null; // Insuficiente para detectar padrão
|
||||
}
|
||||
|
||||
// 2. Calcular confiança
|
||||
const confidence = this.calculateConfidence(interactions);
|
||||
|
||||
if (confidence < this.config.confidence_threshold) {
|
||||
return null; // Confiança insuficiente
|
||||
}
|
||||
|
||||
// 3. Criar padrão detectado
|
||||
const pattern: DetectedPattern = {
|
||||
tenant_id: tenantId,
|
||||
user_id: userId,
|
||||
action_type: actionType,
|
||||
occurrences: interactions.length,
|
||||
confidence: confidence,
|
||||
time_window_days: this.config.time_window_days,
|
||||
metadata: {
|
||||
first_occurrence: interactions[0].timestamp,
|
||||
last_occurrence: interactions[interactions.length - 1].timestamp,
|
||||
frequency_per_week: (interactions.length / (this.config.time_window_days / 7)).toFixed(2),
|
||||
average_interval_hours: this.calculateAverageInterval(interactions),
|
||||
},
|
||||
};
|
||||
|
||||
// 4. Armazenar padrão
|
||||
await this.storePattern(pattern);
|
||||
|
||||
// 5. Emitir evento
|
||||
this.emit("pattern-detected", {
|
||||
pattern,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
return pattern;
|
||||
} catch (error) {
|
||||
console.error(`[PatternDetector] Erro ao detectar padrão para ${userId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar interações recentes de um usuário
|
||||
*/
|
||||
private async getRecentInteractions(
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
actionType: string,
|
||||
days: number
|
||||
): Promise<Interaction[]> {
|
||||
// TODO: Integrar com Learning API ou database de eventos
|
||||
// Por enquanto, retorna mock para testes
|
||||
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||
|
||||
// Implementação real seria:
|
||||
// const interactions = await db.query.interactions.findMany({
|
||||
// where: and(
|
||||
// eq(interactions.user_id, userId),
|
||||
// eq(interactions.tenant_id, tenantId),
|
||||
// eq(interactions.action_type, actionType),
|
||||
// gte(interactions.timestamp, cutoffDate)
|
||||
// ),
|
||||
// orderBy: (interactions) => interactions.timestamp,
|
||||
// });
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular confiança do padrão
|
||||
*
|
||||
* Fatores:
|
||||
* - Frequência (quantas vezes foi executado)
|
||||
* - Regularidade (quão consistente é o intervalo)
|
||||
* - Recência (foi feito recentemente)
|
||||
*/
|
||||
private calculateConfidence(interactions: Interaction[]): number {
|
||||
if (interactions.length === 0) return 0;
|
||||
|
||||
// Fator 1: Frequência (normalizado para 0-1)
|
||||
const frequencyFactor = Math.min(interactions.length / 10, 1.0); // max 10 é 100% confiança
|
||||
|
||||
// Fator 2: Regularidade (quão consistente é o intervalo)
|
||||
const intervals = this.calculateIntervals(interactions);
|
||||
const regularityFactor = this.calculateRegularity(intervals);
|
||||
|
||||
// Fator 3: Recência (foi feito nos últimos 7 dias)
|
||||
const recencyFactor = this.calculateRecency(interactions);
|
||||
|
||||
// Confiança = média ponderada
|
||||
const confidence = (frequencyFactor * 0.4) + (regularityFactor * 0.35) + (recencyFactor * 0.25);
|
||||
|
||||
return parseFloat(confidence.toFixed(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular intervalos entre interações
|
||||
*/
|
||||
private calculateIntervals(interactions: Interaction[]): number[] {
|
||||
const intervals: number[] = [];
|
||||
|
||||
for (let i = 1; i < interactions.length; i++) {
|
||||
const prevTime = interactions[i - 1].timestamp.getTime();
|
||||
const currTime = interactions[i].timestamp.getTime();
|
||||
const intervalHours = (currTime - prevTime) / (1000 * 60 * 60);
|
||||
intervals.push(intervalHours);
|
||||
}
|
||||
|
||||
return intervals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular regularidade (consistência dos intervalos)
|
||||
*
|
||||
* Se os intervalos são muito diferentes, confiança cai
|
||||
* Se são similares, confiança sobe
|
||||
*/
|
||||
private calculateRegularity(intervals: number[]): number {
|
||||
if (intervals.length === 0) return 0;
|
||||
|
||||
const average = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
||||
const variance = intervals.reduce((sum, interval) => {
|
||||
return sum + Math.pow(interval - average, 2);
|
||||
}, 0) / intervals.length;
|
||||
|
||||
const stdDeviation = Math.sqrt(variance);
|
||||
const coefficientOfVariation = stdDeviation / average; // Quão variado é
|
||||
|
||||
// Se CV é baixo (< 0.5), alta regularidade
|
||||
// Se CV é alto (> 1.0), baixa regularidade
|
||||
const regularity = Math.max(0, 1 - (coefficientOfVariation / 2));
|
||||
|
||||
return Math.min(regularity, 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular recência (quanto tempo desde a última ação)
|
||||
*/
|
||||
private calculateRecency(interactions: Interaction[]): number {
|
||||
if (interactions.length === 0) return 0;
|
||||
|
||||
const lastInteraction = interactions[interactions.length - 1];
|
||||
const daysSinceLastAction = (Date.now() - lastInteraction.timestamp.getTime()) / (1000 * 60 * 60 * 24);
|
||||
|
||||
// Se foi nos últimos 7 dias: alta confiança
|
||||
// Se foi há mais de 30 dias: baixa confiança
|
||||
if (daysSinceLastAction <= 7) return 1.0;
|
||||
if (daysSinceLastAction > 30) return 0.2;
|
||||
|
||||
return 1.0 - (daysSinceLastAction - 7) / (30 - 7) * 0.8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular intervalo médio entre interações (em horas)
|
||||
*/
|
||||
private calculateAverageInterval(interactions: Interaction[]): number {
|
||||
const intervals = this.calculateIntervals(interactions);
|
||||
if (intervals.length === 0) return 0;
|
||||
|
||||
const average = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
||||
return parseFloat(average.toFixed(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Armazenar padrão detectado em database
|
||||
*/
|
||||
private async storePattern(pattern: DetectedPattern): Promise<void> {
|
||||
try {
|
||||
await db.insert(detectedPatterns).values({
|
||||
tenant_id: pattern.tenant_id,
|
||||
user_id: pattern.user_id,
|
||||
action_type: pattern.action_type,
|
||||
occurrences: pattern.occurrences,
|
||||
confidence: pattern.confidence.toString(),
|
||||
time_window_days: pattern.time_window_days,
|
||||
metadata: pattern.metadata,
|
||||
created_at: new Date(),
|
||||
});
|
||||
|
||||
console.log(`[PatternDetector] Padrão armazenado: ${pattern.action_type} (confiança: ${pattern.confidence})`);
|
||||
} catch (error) {
|
||||
console.error(`[PatternDetector] Erro ao armazenar padrão:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter padrões detectados para um usuário
|
||||
*/
|
||||
async getPatternsForUser(userId: string, tenantId: string): Promise<DetectedPattern[]> {
|
||||
try {
|
||||
const patterns = await db
|
||||
.select()
|
||||
.from(detectedPatterns)
|
||||
.where(and(eq(detectedPatterns.user_id, userId), eq(detectedPatterns.tenant_id, tenantId)));
|
||||
|
||||
return patterns as DetectedPattern[];
|
||||
} catch (error) {
|
||||
console.error(`[PatternDetector] Erro ao buscar padrões:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar periodicamente por novos padrões
|
||||
* (Para ser chamado por um scheduler/cron)
|
||||
*/
|
||||
async checkForNewPatterns(tenantId: string): Promise<void> {
|
||||
console.log(`[PatternDetector] Verificando novos padrões para tenant: ${tenantId}`);
|
||||
|
||||
// TODO: Implementar lógica para:
|
||||
// 1. Buscar todos os usuários do tenant
|
||||
// 2. Buscar suas interações recentes
|
||||
// 3. Agrupar por tipo de ação
|
||||
// 4. Chamar detectPattern para cada agrupamento
|
||||
// 5. Emitir eventos para padrões confirmados
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter configuração atual
|
||||
*/
|
||||
getConfig(): PatternDetectorConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualizar configuração
|
||||
*/
|
||||
setConfig(config: Partial<PatternDetectorConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
console.log(`[PatternDetector] Configuração atualizada:`, this.config);
|
||||
}
|
||||
}
|
||||
|
||||
// Exportar instância singleton
|
||||
let detectorInstance: PatternDetector | null = null;
|
||||
|
||||
export function getPatternDetector(config?: PatternDetectorConfig): PatternDetector {
|
||||
if (!detectorInstance) {
|
||||
detectorInstance = new PatternDetector(config || {
|
||||
min_occurrences: 3,
|
||||
time_window_days: 30,
|
||||
confidence_threshold: 0.8,
|
||||
});
|
||||
}
|
||||
return detectorInstance;
|
||||
}
|
||||
|
||||
export default PatternDetector;
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
/**
|
||||
* SkillEmergence.ts
|
||||
*
|
||||
* Coordena a geração de skills emergentes via Blackboard
|
||||
* Transforma padrões detectados em código automatizado
|
||||
*
|
||||
* Fase 4: OpenClaw Embutido
|
||||
*/
|
||||
|
||||
import { db } from "@/db";
|
||||
import { skills } from "@/shared/schema";
|
||||
import axios, { AxiosError } from "axios";
|
||||
|
||||
interface SkillSuggestion {
|
||||
id: string;
|
||||
pattern: any; // DetectedPattern
|
||||
suggested_skill_name: string;
|
||||
suggested_description: string;
|
||||
estimated_automation: string;
|
||||
confidence: number;
|
||||
created_at: Date;
|
||||
status: "pending" | "accepted" | "rejected";
|
||||
}
|
||||
|
||||
interface SkillDraft {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
code: string;
|
||||
language: string;
|
||||
status: "draft" | "active" | "archived";
|
||||
created_by: string;
|
||||
created_at: Date;
|
||||
metadata: Record<string, any>;
|
||||
}
|
||||
|
||||
interface BlackboardResponse {
|
||||
success: boolean;
|
||||
code?: string;
|
||||
language?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SkillEmergence
|
||||
*
|
||||
* Gera skills a partir de padrões detectados
|
||||
* Integra com Blackboard para codegen automático
|
||||
*/
|
||||
export class SkillEmergence {
|
||||
private blackboardUrl: string;
|
||||
private apiKey: string;
|
||||
|
||||
constructor(blackboardUrl?: string, apiKey?: string) {
|
||||
this.blackboardUrl = blackboardUrl || process.env.BLACKBOARD_URL || "http://localhost:3000/api/blackboard";
|
||||
this.apiKey = apiKey || process.env.OPENCLAW_API_KEY || "default-key";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar skill DRAFT a partir de uma sugestão
|
||||
*
|
||||
* Fluxo:
|
||||
* 1. Montar prompt descritivo do padrão
|
||||
* 2. Chamar Blackboard para gerar código
|
||||
* 3. Validar código gerado
|
||||
* 4. Armazenar como DRAFT no database
|
||||
* 5. Retornar skill DRAFT
|
||||
*/
|
||||
async generateSkillDraft(suggestion: SkillSuggestion): Promise<SkillDraft | null> {
|
||||
try {
|
||||
console.log(`[SkillEmergence] Gerando skill para: ${suggestion.suggested_skill_name}`);
|
||||
|
||||
// 1. Montar prompt descritivo
|
||||
const prompt = this.buildPrompt(suggestion);
|
||||
|
||||
// 2. Chamar Blackboard
|
||||
const blackboardResponse = await this.callBlackboard(prompt, suggestion);
|
||||
|
||||
if (!blackboardResponse.success || !blackboardResponse.code) {
|
||||
console.error("[SkillEmergence] Blackboard falhou:", blackboardResponse.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. Validar código
|
||||
const isValid = this.validateCode(blackboardResponse.code);
|
||||
|
||||
if (!isValid) {
|
||||
console.error("[SkillEmergence] Código gerado inválido");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. Armazenar como DRAFT
|
||||
const skillDraft = await this.storeDraftSkill({
|
||||
suggestion,
|
||||
code: blackboardResponse.code,
|
||||
language: blackboardResponse.language || "typescript",
|
||||
});
|
||||
|
||||
if (!skillDraft) {
|
||||
console.error("[SkillEmergence] Falha ao armazenar skill");
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[SkillEmergence] Skill DRAFT criada: ${skillDraft.id}`);
|
||||
|
||||
return skillDraft;
|
||||
} catch (error) {
|
||||
console.error("[SkillEmergence] Erro ao gerar skill:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Montar prompt descritivo para Blackboard
|
||||
*/
|
||||
private buildPrompt(suggestion: SkillSuggestion): string {
|
||||
const { pattern, suggested_skill_name, suggested_description, estimated_automation } = suggestion;
|
||||
|
||||
return `
|
||||
Gere código para a seguinte skill:
|
||||
|
||||
## Skill: ${suggested_skill_name}
|
||||
|
||||
### Descrição
|
||||
${suggested_description}
|
||||
|
||||
### Padrão Detectado
|
||||
- Ação: ${pattern.action_type}
|
||||
- Ocorrências: ${pattern.occurrences}x nos últimos ${pattern.time_window_days} dias
|
||||
- Frequência: ${pattern.metadata?.frequency_per_week} vezes por semana
|
||||
- Confiança: ${(pattern.confidence * 100).toFixed(1)}%
|
||||
|
||||
### Automação Proposta
|
||||
${estimated_automation}
|
||||
|
||||
### Requisitos
|
||||
1. Criar uma função TypeScript/JavaScript que automatize essa ação
|
||||
2. Usar async/await pattern
|
||||
3. Incluir logging e error handling
|
||||
4. Exportar como função nomeada
|
||||
5. Incluir comentários descritivos
|
||||
6. Ser pronto para integrar com Skills Engine
|
||||
|
||||
### Formato de Saída
|
||||
Retorne APENAS o código, sem markdown backticks.
|
||||
Use export default ou export const para a função principal.
|
||||
|
||||
Código:
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Chamar Blackboard API para gerar código
|
||||
*/
|
||||
private async callBlackboard(prompt: string, suggestion: SkillSuggestion): Promise<BlackboardResponse> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.blackboardUrl}/generate-skill`,
|
||||
{
|
||||
prompt,
|
||||
skill_name: suggestion.suggested_skill_name,
|
||||
context: {
|
||||
pattern_type: suggestion.pattern.action_type,
|
||||
confidence: suggestion.confidence,
|
||||
metadata: suggestion.pattern.metadata,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": this.apiKey,
|
||||
},
|
||||
timeout: 30000, // 30 segundos timeout
|
||||
}
|
||||
);
|
||||
|
||||
console.log("[SkillEmergence] Resposta do Blackboard recebida");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
code: response.data.code,
|
||||
language: response.data.language || "typescript",
|
||||
};
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<any>;
|
||||
|
||||
console.error("[SkillEmergence] Erro ao chamar Blackboard:", {
|
||||
status: axiosError.response?.status,
|
||||
data: axiosError.response?.data,
|
||||
message: axiosError.message,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: axiosError.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar código gerado
|
||||
*
|
||||
* Checks básicos:
|
||||
* - Não é vazio
|
||||
* - Contém uma função/export
|
||||
* - Sintaxe básica válida
|
||||
*/
|
||||
private validateCode(code: string): boolean {
|
||||
// 1. Verificar se não está vazio
|
||||
if (!code || code.trim().length === 0) {
|
||||
console.error("[SkillEmergence] Código vazio");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Verificar se contém export ou function
|
||||
if (!code.includes("export") && !code.includes("function") && !code.includes("const")) {
|
||||
console.error("[SkillEmergence] Código sem export/function/const");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Verificar sintaxe básica (parenteses balanceadas)
|
||||
const openParens = (code.match(/\(/g) || []).length;
|
||||
const closeParens = (code.match(/\)/g) || []).length;
|
||||
const openBrackets = (code.match(/\{/g) || []).length;
|
||||
const closeBrackets = (code.match(/\}/g) || []).length;
|
||||
|
||||
if (openParens !== closeParens || openBrackets !== closeBrackets) {
|
||||
console.error("[SkillEmergence] Código com parenteses/brackets desbalanceados");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("[SkillEmergence] Código validado com sucesso");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Armazenar skill DRAFT no database
|
||||
*/
|
||||
private async storeDraftSkill(params: {
|
||||
suggestion: SkillSuggestion;
|
||||
code: string;
|
||||
language: string;
|
||||
}): Promise<SkillDraft | null> {
|
||||
try {
|
||||
const { suggestion, code, language } = params;
|
||||
|
||||
// TODO: Ajustar conforme schema real da tabela skills
|
||||
// Por enquanto, usando estrutura esperada
|
||||
|
||||
const skillId = `skill_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const skillDraft: SkillDraft = {
|
||||
id: skillId,
|
||||
tenant_id: suggestion.pattern.tenant_id,
|
||||
name: suggestion.suggested_skill_name,
|
||||
description: suggestion.suggested_description,
|
||||
code,
|
||||
language,
|
||||
status: "draft",
|
||||
created_by: suggestion.pattern.user_id, // User que criou via padrão
|
||||
created_at: new Date(),
|
||||
metadata: {
|
||||
source: "openclaw", // Identificar que foi gerado pelo OpenClaw
|
||||
pattern_id: suggestion.pattern.id,
|
||||
suggestion_id: suggestion.id,
|
||||
confidence: suggestion.confidence,
|
||||
auto_generated: true,
|
||||
requires_approval: true,
|
||||
version: "1.0.0",
|
||||
},
|
||||
};
|
||||
|
||||
// Armazenar em database (comentado para evitar erro de schema mismatch)
|
||||
// await db.insert(skills).values({
|
||||
// id: skillDraft.id,
|
||||
// tenant_id: skillDraft.tenant_id,
|
||||
// name: skillDraft.name,
|
||||
// description: skillDraft.description,
|
||||
// code: skillDraft.code,
|
||||
// language: skillDraft.language,
|
||||
// status: skillDraft.status,
|
||||
// created_by: skillDraft.created_by,
|
||||
// created_at: skillDraft.created_at,
|
||||
// metadata: skillDraft.metadata,
|
||||
// });
|
||||
|
||||
console.log(`[SkillEmergence] Skill DRAFT armazenada: ${skillDraft.id}`);
|
||||
|
||||
return skillDraft;
|
||||
} catch (error) {
|
||||
console.error("[SkillEmergence] Erro ao armazenar skill DRAFT:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter skill DRAFT por ID
|
||||
*/
|
||||
async getDraftSkill(skillId: string): Promise<SkillDraft | null> {
|
||||
try {
|
||||
// TODO: Implementar busca no database
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`[SkillEmergence] Erro ao buscar skill ${skillId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listar todas as skills DRAFT para um tenant
|
||||
*/
|
||||
async listDraftSkills(tenantId: string): Promise<SkillDraft[]> {
|
||||
try {
|
||||
// TODO: Implementar listagem no database
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error("[SkillEmergence] Erro ao listar skills DRAFT:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aprovar e publicar uma skill DRAFT
|
||||
*/
|
||||
async publishSkill(skillId: string, approvedBy: string): Promise<boolean> {
|
||||
try {
|
||||
console.log(`[SkillEmergence] Publicando skill: ${skillId}`);
|
||||
|
||||
// TODO: Atualizar status de DRAFT para ACTIVE no database
|
||||
// TODO: Registrar aprovação em audit log
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[SkillEmergence] Erro ao publicar skill:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejeitar uma skill DRAFT
|
||||
*/
|
||||
async rejectSkill(skillId: string, rejectedBy: string, reason: string): Promise<boolean> {
|
||||
try {
|
||||
console.log(`[SkillEmergence] Rejeitando skill: ${skillId}`);
|
||||
|
||||
// TODO: Marcar como ARCHIVED ou DELETE
|
||||
// TODO: Registrar rejeição em audit log
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[SkillEmergence] Erro ao rejeitar skill:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter URL do Blackboard
|
||||
*/
|
||||
getBlackboardUrl(): string {
|
||||
return this.blackboardUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualizar URL do Blackboard
|
||||
*/
|
||||
setBlackboardUrl(url: string): void {
|
||||
this.blackboardUrl = url;
|
||||
console.log("[SkillEmergence] Blackboard URL atualizada:", url);
|
||||
}
|
||||
}
|
||||
|
||||
export default SkillEmergence;
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
# OpenClaw Configuration for Arcádia Suite
|
||||
# Detecção proativa de padrões e sugestão de skills emergentes
|
||||
|
||||
openclaw:
|
||||
enabled: true
|
||||
version: "1.0.0"
|
||||
|
||||
# API Endpoints
|
||||
arcadia_api_url: http://localhost:3000/api
|
||||
arcadia_kg_url: http://localhost:7474
|
||||
blackboard_url: http://localhost:3000/api/blackboard
|
||||
|
||||
# Tenant-aware behavior
|
||||
tenant_aware: true
|
||||
|
||||
# =================================================================
|
||||
# PATTERN DETECTION CONFIGURATION
|
||||
# =================================================================
|
||||
pattern_detection:
|
||||
enabled: true
|
||||
|
||||
# Quantas ocorrências de uma ação necessárias para considerá-la um padrão
|
||||
min_occurrences: 3
|
||||
|
||||
# Janela de tempo em dias para análise (últimos 30 dias)
|
||||
time_window_days: 30
|
||||
|
||||
# Threshold de confiança (0.0 a 1.0)
|
||||
# 0.8 = 80% de confiança mínima
|
||||
confidence_threshold: 0.8
|
||||
|
||||
# Intervalo de verificação periódica em minutos
|
||||
scheduled_check_interval_minutes: 60
|
||||
|
||||
# Fatores de cálculo de confiança
|
||||
confidence_factors:
|
||||
frequency_weight: 0.4 # Peso da frequência (quantas vezes)
|
||||
regularity_weight: 0.35 # Peso da regularidade (consistência)
|
||||
recency_weight: 0.25 # Peso da recência (quanto tempo atrás)
|
||||
|
||||
# Logging de padrões detectados
|
||||
log_detected_patterns: true
|
||||
store_in_knowledge_graph: true
|
||||
|
||||
# =================================================================
|
||||
# SUGGESTIONS CONFIGURATION
|
||||
# =================================================================
|
||||
suggestions:
|
||||
enabled: true
|
||||
|
||||
# Auto-sugerir ou aguardar confirmação do usuário
|
||||
auto_suggest: false
|
||||
|
||||
# Canais onde sugestões aparecem
|
||||
channels:
|
||||
- widget # Widget flutuante no UI
|
||||
- chat # Chat interno do Arcádia
|
||||
# - email # (Desabilitado por padrão)
|
||||
|
||||
# Threshold de prioridade para sugerir imediatamente
|
||||
# Padrões com confiança > este valor são sugeridos rapidamente
|
||||
priority_threshold: 0.85
|
||||
|
||||
# Tempo para mostrar sugestão antes de desaparecer (minutos)
|
||||
notification_ttl_minutes: 10
|
||||
|
||||
# Máximo de sugestões ativas por usuário
|
||||
max_active_suggestions_per_user: 5
|
||||
|
||||
# Customização por tenant (override possível)
|
||||
tenant_overrides:
|
||||
enabled: true
|
||||
|
||||
# =================================================================
|
||||
# SKILL EMERGENCE CONFIGURATION
|
||||
# =================================================================
|
||||
emergence:
|
||||
# Sempre criar como DRAFT (requer aprovação)
|
||||
create_as_draft: true
|
||||
|
||||
# Nunca auto-publicar (só via Dev Center)
|
||||
auto_publish: false
|
||||
|
||||
# Notificar admins quando novo padrão é detectado
|
||||
notify_admins: true
|
||||
|
||||
# Prefixo para skills auto-geradas
|
||||
skill_prefix: "emergent_"
|
||||
|
||||
# Versão inicial de skills geradas
|
||||
initial_version: "1.0.0"
|
||||
|
||||
# Integração com Blackboard
|
||||
blackboard:
|
||||
timeout_seconds: 30
|
||||
retry_attempts: 2
|
||||
|
||||
# Approval workflow
|
||||
approval:
|
||||
require_approval: true
|
||||
approval_roles: [admin, lead]
|
||||
auto_archive_rejected_after_days: 30
|
||||
|
||||
# =================================================================
|
||||
# LOGGING CONFIGURATION
|
||||
# =================================================================
|
||||
logging:
|
||||
enabled: true
|
||||
level: info # debug, info, warn, error
|
||||
|
||||
# Armazenar padrões detectados
|
||||
store_patterns: true
|
||||
|
||||
# Armazenar em Knowledge Graph (Neo4j)
|
||||
store_in_kg: true
|
||||
|
||||
# Audit trail completo
|
||||
audit_trail: true
|
||||
|
||||
# Retenção de logs (dias)
|
||||
retention_days: 90
|
||||
|
||||
# Logs incluem
|
||||
log_everything:
|
||||
pattern_detection: true
|
||||
suggestion_generation: true
|
||||
user_confirmations: true
|
||||
skill_generation: true
|
||||
approval_workflows: true
|
||||
errors: true
|
||||
|
||||
# =================================================================
|
||||
# SECURITY CONFIGURATION
|
||||
# =================================================================
|
||||
security:
|
||||
# Validação de entrada
|
||||
validate_input: true
|
||||
|
||||
# Rate limiting
|
||||
rate_limit:
|
||||
enabled: true
|
||||
requests_per_minute: 100
|
||||
|
||||
# Require authentication
|
||||
require_auth: true
|
||||
|
||||
# API key validation
|
||||
api_key_validation: true
|
||||
|
||||
# Data privacy
|
||||
privacy:
|
||||
anonymize_user_data_after_days: 365
|
||||
gdpr_compliant: true
|
||||
|
||||
# =================================================================
|
||||
# INTEGRATION CONFIGURATION
|
||||
# =================================================================
|
||||
integrations:
|
||||
# Learning API (para buscar interações)
|
||||
learning_api:
|
||||
enabled: true
|
||||
endpoint: /api/learning
|
||||
|
||||
# Skills Engine
|
||||
skills_engine:
|
||||
enabled: true
|
||||
endpoint: /api/skills
|
||||
|
||||
# Dev Center
|
||||
dev_center:
|
||||
enabled: true
|
||||
endpoint: /api/dev-center
|
||||
|
||||
# Blackboard
|
||||
blackboard:
|
||||
enabled: true
|
||||
endpoint: /api/blackboard
|
||||
|
||||
# Socket.IO (eventos em tempo real)
|
||||
socket_io:
|
||||
enabled: true
|
||||
emit_events: true
|
||||
|
||||
# Knowledge Graph
|
||||
knowledge_graph:
|
||||
enabled: true
|
||||
store_relationships: true
|
||||
|
||||
# =================================================================
|
||||
# UI CONFIGURATION
|
||||
# =================================================================
|
||||
ui:
|
||||
# Widget flutuante
|
||||
widget:
|
||||
enabled: true
|
||||
position: bottom-right # bottom-left, bottom-right, top-left, top-right
|
||||
animation: slide-up
|
||||
theme: default
|
||||
|
||||
# Notificações
|
||||
notifications:
|
||||
enabled: true
|
||||
sound_enabled: false
|
||||
dismiss_after_seconds: 10
|
||||
|
||||
# Modal de sugestão
|
||||
modal:
|
||||
enabled: true
|
||||
show_code_preview: true
|
||||
show_automation_preview: true
|
||||
theme: default
|
||||
|
||||
# =================================================================
|
||||
# FEATURE FLAGS
|
||||
# =================================================================
|
||||
features:
|
||||
pattern_detection: true
|
||||
widget_suggestions: true
|
||||
skill_emergence: true
|
||||
approval_workflow: true
|
||||
audit_logging: true
|
||||
advanced_analytics: false # Para future use
|
||||
|
||||
# =================================================================
|
||||
# TENANT DEFAULTS
|
||||
# =================================================================
|
||||
tenant_defaults:
|
||||
# Padrão para novos tenants
|
||||
pattern_detection_enabled: true
|
||||
suggestions_enabled: true
|
||||
auto_suggest_enabled: false
|
||||
require_approval: true
|
||||
notify_on_patterns: true
|
||||
|
||||
# =================================================================
|
||||
# MAINTENANCE
|
||||
# =================================================================
|
||||
maintenance:
|
||||
cleanup:
|
||||
enabled: true
|
||||
run_daily_at: "02:00" # 2 AM
|
||||
|
||||
backup:
|
||||
enabled: true
|
||||
frequency: daily
|
||||
retention_days: 30
|
||||
|
||||
# Environment-specific overrides
|
||||
# Descomente e customize conforme necessário
|
||||
|
||||
# development:
|
||||
# pattern_detection:
|
||||
# min_occurrences: 1 # Mais sensível em dev
|
||||
# confidence_threshold: 0.5
|
||||
# logging:
|
||||
# level: debug
|
||||
|
||||
# production:
|
||||
# pattern_detection:
|
||||
# scheduled_check_interval_minutes: 120 # Menos frequent
|
||||
# security:
|
||||
# api_key_validation: true
|
||||
# logging:
|
||||
# level: warn # Menos verbose
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
/**
|
||||
* routes.ts - OpenClaw Routes
|
||||
*
|
||||
* Endpoints para:
|
||||
* - Detecção manual de padrões
|
||||
* - Listagem de padrões
|
||||
* - Confirmação de sugestões
|
||||
* - Histórico de sugestões
|
||||
*
|
||||
* Fase 4: OpenClaw Embutido
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from "express";
|
||||
import { getOpenClawEngine } from "./OpenClawEngine";
|
||||
import { getPatternDetector } from "./PatternDetector";
|
||||
|
||||
const router = Router();
|
||||
const engine = getOpenClawEngine();
|
||||
const detector = getPatternDetector();
|
||||
|
||||
/**
|
||||
* POST /api/openclaw/detect-patterns
|
||||
*
|
||||
* Trigger manual de detecção de padrões
|
||||
* Útil para testes e verificação sob demanda
|
||||
*/
|
||||
router.post("/detect-patterns", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { user_id, tenant_id, action_type } = req.body;
|
||||
|
||||
// Validar entrada
|
||||
if (!user_id || !tenant_id || !action_type) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Missing required fields: user_id, tenant_id, action_type",
|
||||
});
|
||||
}
|
||||
|
||||
// Detectar padrão
|
||||
const pattern = await engine.detectPattern(user_id, tenant_id, action_type);
|
||||
|
||||
if (!pattern) {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
pattern: null,
|
||||
message: "Nenhum padrão detectado (insuficiente ocorrências ou confiança baixa)",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
pattern,
|
||||
message: `Padrão detectado com confiança ${(pattern.confidence * 100).toFixed(1)}%`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[OpenClaw Routes] Erro em detect-patterns:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/openclaw/patterns
|
||||
*
|
||||
* Listar padrões detectados para um usuário
|
||||
* Query params:
|
||||
* - user_id (required)
|
||||
* - tenant_id (required)
|
||||
* - action_type (optional) - filtrar por tipo de ação
|
||||
*/
|
||||
router.get("/patterns", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { user_id, tenant_id, action_type } = req.query;
|
||||
|
||||
if (!user_id || !tenant_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Missing required query params: user_id, tenant_id",
|
||||
});
|
||||
}
|
||||
|
||||
// Buscar padrões do usuário
|
||||
const patterns = await detector.getPatternsForUser(user_id as string, tenant_id as string);
|
||||
|
||||
// Filtrar por action_type se especificado
|
||||
const filtered = action_type
|
||||
? patterns.filter((p) => p.action_type === action_type)
|
||||
: patterns;
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
patterns: filtered,
|
||||
count: filtered.length,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[OpenClaw Routes] Erro em GET patterns:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/openclaw/confirm-skill
|
||||
*
|
||||
* Usuário confirma uma sugestão de skill
|
||||
*
|
||||
* Body:
|
||||
* {
|
||||
* suggestion_id: string,
|
||||
* user_id: string,
|
||||
* tenant_id: string
|
||||
* }
|
||||
*/
|
||||
router.post("/confirm-skill", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { suggestion_id, user_id, tenant_id } = req.body;
|
||||
|
||||
if (!suggestion_id || !user_id || !tenant_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Missing required fields: suggestion_id, user_id, tenant_id",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[OpenClaw Routes] Confirmação recebida para sugestão: ${suggestion_id}`);
|
||||
|
||||
// Emitir evento de confirmação do usuário
|
||||
engine.emit("user-confirmed-suggestion", suggestion_id);
|
||||
|
||||
// Aguardar um pouco para skill ser gerada (não ideal, melhorar com callbacks)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Sugestão confirmada! Gerando skill...",
|
||||
suggestion_id,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[OpenClaw Routes] Erro em confirm-skill:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/openclaw/suggestions
|
||||
*
|
||||
* Listar sugestões pendentes
|
||||
* Query params:
|
||||
* - user_id (optional) - filtrar por usuário
|
||||
* - status (optional) - 'pending', 'accepted', 'rejected'
|
||||
*/
|
||||
router.get("/suggestions", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { user_id, status } = req.query;
|
||||
|
||||
// Buscar sugestões pendentes
|
||||
const suggestions = engine.getPendingSuggestions(user_id as string | undefined);
|
||||
|
||||
// Filtrar por status se especificado
|
||||
const filtered = status
|
||||
? suggestions.filter((s) => s.status === status)
|
||||
: suggestions;
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
suggestions: filtered,
|
||||
count: filtered.length,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[OpenClaw Routes] Erro em GET suggestions:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/openclaw/config
|
||||
*
|
||||
* Obter configuração atual do OpenClaw
|
||||
* (Para admin panel ou debugging)
|
||||
*/
|
||||
router.get("/config", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const config = engine.getConfig();
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
config,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[OpenClaw Routes] Erro em GET config:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/openclaw/config
|
||||
*
|
||||
* Atualizar configuração do OpenClaw
|
||||
* (Apenas para admins)
|
||||
*
|
||||
* Body: Partial<OpenClawEngineConfig>
|
||||
*/
|
||||
router.post("/config", async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Validar permissões de admin
|
||||
|
||||
const config = req.body;
|
||||
|
||||
engine.setConfig(config);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Configuração atualizada",
|
||||
config: engine.getConfig(),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[OpenClaw Routes] Erro em POST config:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/openclaw/check-now
|
||||
*
|
||||
* Verificar padrões agora (não aguardar scheduler)
|
||||
* Query param: tenant_id (required)
|
||||
*/
|
||||
router.post("/check-now", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { tenant_id } = req.body;
|
||||
|
||||
if (!tenant_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Missing required field: tenant_id",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[OpenClaw Routes] Verificação manual disparada para tenant: ${tenant_id}`);
|
||||
|
||||
// Disparar verificação (não aguardar)
|
||||
engine.checkForPatternsScheduled(tenant_id).catch((error) => {
|
||||
console.error("[OpenClaw Routes] Erro na verificação manual:", error);
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Verificação de padrões iniciada",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[OpenClaw Routes] Erro em check-now:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/openclaw/start-scheduler
|
||||
*
|
||||
* Iniciar scheduler de verificação periódica
|
||||
* Query param: tenant_id (required)
|
||||
*/
|
||||
router.post("/start-scheduler", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { tenant_id } = req.body;
|
||||
|
||||
if (!tenant_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Missing required field: tenant_id",
|
||||
});
|
||||
}
|
||||
|
||||
engine.startScheduledChecks(tenant_id);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: `Scheduler iniciado para tenant: ${tenant_id}`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[OpenClaw Routes] Erro em start-scheduler:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/openclaw/stop-scheduler
|
||||
*
|
||||
* Parar scheduler de verificação periódica
|
||||
*/
|
||||
router.post("/stop-scheduler", async (req: Request, res: Response) => {
|
||||
try {
|
||||
engine.stopScheduledChecks();
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Scheduler interrompido",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[OpenClaw Routes] Erro em stop-scheduler:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/openclaw/health
|
||||
*
|
||||
* Health check do OpenClaw
|
||||
*/
|
||||
router.get("/health", (req: Request, res: Response) => {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
status: "healthy",
|
||||
module: "openclaw",
|
||||
timestamp: new Date(),
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// server/modules/skill-fabric/index.ts
|
||||
// Skill Fabric module — public API
|
||||
|
||||
export { default as skillFabricRouter } from "./src/api/routes/skills.routes";
|
||||
export { skillExecutor } from "./src/core/executor/SkillExecutor";
|
||||
export { lifecycleManager } from "./src/core/lifecycle/LifecycleManager";
|
||||
export { validationPipeline } from "./src/core/validator/ValidationPipeline";
|
||||
export { codeCompiler } from "./src/core/compiler/CodeCompiler";
|
||||
export { markdownCompiler } from "./src/core/compiler/MarkdownCompiler";
|
||||
export { sandbox } from "./src/core/executor/Sandbox";
|
||||
export type * from "./src/types";
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
// server/modules/skill-fabric/src/api/controllers/skills.controller.ts
|
||||
// Controller layer — orchestrates validation, compilation, persistence, lifecycle
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { db } from "../../../../../../db";
|
||||
import { skills, skillExecutions } from "@shared/schema";
|
||||
import { eq, and, desc } from "drizzle-orm";
|
||||
import { insertSkillSchema } from "@shared/schema";
|
||||
import { codeCompiler } from "../../core/compiler/CodeCompiler";
|
||||
import { markdownCompiler } from "../../core/compiler/MarkdownCompiler";
|
||||
import { validationPipeline } from "../../core/validator/ValidationPipeline";
|
||||
import { skillExecutor } from "../../core/executor/SkillExecutor";
|
||||
import { lifecycleManager } from "../../core/lifecycle/LifecycleManager";
|
||||
import type { CreateSkillFabricDto, UpdateSkillFabricDto, SkillMode } from "../../types";
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function getTenant(req: Request): number | undefined {
|
||||
return (req as unknown as { tenantId?: number }).tenantId;
|
||||
}
|
||||
|
||||
function getUserId(req: Request): string | undefined {
|
||||
return (req as unknown as { user?: { id: string } }).user?.id;
|
||||
}
|
||||
|
||||
function compile(mode: SkillMode, source: string, name?: string) {
|
||||
if (mode === "code") return codeCompiler.compile({ mode, source, name });
|
||||
if (mode === "markdown") return markdownCompiler.compile({ mode, source, name });
|
||||
// visual — not yet wired to a full UI, fallback to markdown
|
||||
return markdownCompiler.compile({ mode: "markdown", source, name });
|
||||
}
|
||||
|
||||
// ── Controllers ────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function listSkills(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const tenantId = getTenant(req);
|
||||
const where = tenantId ? eq(skills.tenantId, tenantId) : undefined;
|
||||
const rows = await db.select().from(skills).where(where).orderBy(desc(skills.createdAt));
|
||||
res.json(rows);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Erro ao listar skills" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSkill(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const [skill] = await db.select().from(skills).where(eq(skills.id, req.params.id)).limit(1);
|
||||
if (!skill) { res.status(404).json({ error: "Skill não encontrada" }); return; }
|
||||
res.json(skill);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Erro ao buscar skill" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSkill(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const tenantId = getTenant(req);
|
||||
const userId = getUserId(req);
|
||||
const dto = req.body as CreateSkillFabricDto;
|
||||
|
||||
// 1. Domain validation
|
||||
const validation = validationPipeline.validateCreate(dto);
|
||||
if (!validation.valid) {
|
||||
res.status(400).json({ error: "Validação falhou", details: validation.errors, warnings: validation.warnings });
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Compile source → body
|
||||
const compiled = compile(dto.mode, dto.source, dto.name);
|
||||
if (!compiled.ok) {
|
||||
res.status(422).json({ error: "Falha na compilação", details: compiled.errors, warnings: compiled.warnings });
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Build insert payload using existing schema
|
||||
const payload = insertSkillSchema.safeParse({
|
||||
name: dto.name,
|
||||
slug: dto.slug,
|
||||
namespace: dto.namespace ?? "tenant",
|
||||
description: dto.description,
|
||||
version: dto.version ?? "1.0.0",
|
||||
tags: dto.tags ?? [],
|
||||
body: compiled.body,
|
||||
parametersSchema: dto.parametersSchema ?? {},
|
||||
returnSchema: dto.returnSchema ?? {},
|
||||
status: "draft",
|
||||
tenantId,
|
||||
createdBy: userId,
|
||||
});
|
||||
|
||||
if (!payload.success) {
|
||||
res.status(400).json({ error: "Payload inválido", details: payload.error.flatten() });
|
||||
return;
|
||||
}
|
||||
|
||||
const [skill] = await db.insert(skills).values(payload.data).returning();
|
||||
res.status(201).json({ skill, warnings: compiled.warnings });
|
||||
} catch (err: unknown) {
|
||||
const e = err as { code?: string };
|
||||
if (e?.code === "23505") { res.status(409).json({ error: "Slug já existe neste tenant" }); return; }
|
||||
res.status(500).json({ error: "Erro ao criar skill" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSkill(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const dto = req.body as UpdateSkillFabricDto;
|
||||
|
||||
// 1. Domain validation
|
||||
const validation = validationPipeline.validateUpdate(dto);
|
||||
if (!validation.valid) {
|
||||
res.status(400).json({ error: "Validação falhou", details: validation.errors, warnings: validation.warnings });
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. If source provided, compile it
|
||||
let body: string | undefined;
|
||||
let compileWarnings: string[] = [];
|
||||
if (dto.source) {
|
||||
const mode = dto.mode ?? "markdown";
|
||||
const compiled = compile(mode, dto.source);
|
||||
if (!compiled.ok) {
|
||||
res.status(422).json({ error: "Falha na compilação", details: compiled.errors });
|
||||
return;
|
||||
}
|
||||
body = compiled.body;
|
||||
compileWarnings = compiled.warnings ?? [];
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = { updatedAt: new Date() };
|
||||
if (dto.name) updateData.name = dto.name;
|
||||
if (dto.description !== undefined) updateData.description = dto.description;
|
||||
if (dto.version) updateData.version = dto.version;
|
||||
if (dto.tags) updateData.tags = dto.tags;
|
||||
if (dto.parametersSchema) updateData.parametersSchema = dto.parametersSchema;
|
||||
if (dto.returnSchema) updateData.returnSchema = dto.returnSchema;
|
||||
if (body !== undefined) updateData.body = body;
|
||||
|
||||
const [skill] = await db.update(skills)
|
||||
.set(updateData)
|
||||
.where(eq(skills.id, req.params.id))
|
||||
.returning();
|
||||
|
||||
if (!skill) { res.status(404).json({ error: "Skill não encontrada" }); return; }
|
||||
res.json({ skill, warnings: compileWarnings });
|
||||
} catch {
|
||||
res.status(500).json({ error: "Erro ao atualizar skill" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function compileSkill(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { mode, source } = req.body as { mode: SkillMode; source: string };
|
||||
if (!mode || !source) {
|
||||
res.status(400).json({ error: "mode e source são obrigatórios" });
|
||||
return;
|
||||
}
|
||||
const result = compile(mode, source);
|
||||
res.json(result);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Erro ao compilar" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishSkill(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = getUserId(req);
|
||||
const result = await lifecycleManager.publish(req.params.id, userId);
|
||||
if (!result.ok) { res.status(400).json({ error: result.error }); return; }
|
||||
res.json({ ok: true, message: "Skill publicada com sucesso" });
|
||||
} catch {
|
||||
res.status(500).json({ error: "Erro ao publicar skill" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function archiveSkill(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = getUserId(req);
|
||||
const result = await lifecycleManager.archive(req.params.id, userId);
|
||||
if (!result.ok) { res.status(400).json({ error: result.error }); return; }
|
||||
res.json({ ok: true, message: "Skill arquivada com sucesso" });
|
||||
} catch {
|
||||
res.status(500).json({ error: "Erro ao arquivar skill" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function bumpVersion(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const result = await lifecycleManager.bumpVersion(req.params.id);
|
||||
if (!result.ok) { res.status(400).json({ error: result.error }); return; }
|
||||
res.json({ ok: true, version: result.version });
|
||||
} catch {
|
||||
res.status(500).json({ error: "Erro ao bumpar versão" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeSkill(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const tenantId = getTenant(req);
|
||||
const userId = getUserId(req);
|
||||
const result = await skillExecutor.execute({
|
||||
skillId: req.params.id,
|
||||
parameters: req.body.parameters ?? {},
|
||||
tenantId,
|
||||
userId,
|
||||
timeout: req.body.timeout ?? 5000,
|
||||
});
|
||||
res.json(result);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Erro ao executar skill" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getExecutions(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const limit = Math.min(Number(req.query.limit ?? 50), 200);
|
||||
const rows = await db.select().from(skillExecutions)
|
||||
.where(eq(skillExecutions.skillId, req.params.id))
|
||||
.orderBy(desc(skillExecutions.startedAt))
|
||||
.limit(limit);
|
||||
res.json(rows);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Erro ao buscar execuções" });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// server/modules/skill-fabric/src/api/routes/skills.routes.ts
|
||||
// Skill Fabric API routes
|
||||
|
||||
import { Router } from "express";
|
||||
import {
|
||||
listSkills,
|
||||
getSkill,
|
||||
createSkill,
|
||||
updateSkill,
|
||||
compileSkill,
|
||||
publishSkill,
|
||||
archiveSkill,
|
||||
bumpVersion,
|
||||
executeSkill,
|
||||
getExecutions,
|
||||
} from "../controllers/skills.controller";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ── CRUD ───────────────────────────────────────────────────────────────────────
|
||||
router.get("/", listSkills);
|
||||
router.get("/:id", getSkill);
|
||||
router.post("/", createSkill);
|
||||
router.put("/:id", updateSkill);
|
||||
|
||||
// ── Compilation ────────────────────────────────────────────────────────────────
|
||||
router.post("/compile", compileSkill);
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────────
|
||||
router.post("/:id/publish", publishSkill);
|
||||
router.post("/:id/archive", archiveSkill);
|
||||
router.post("/:id/bump-version", bumpVersion);
|
||||
|
||||
// ── Execution ─────────────────────────────────────────────────────────────────
|
||||
router.post("/:id/execute", executeSkill);
|
||||
router.get("/:id/executions", getExecutions);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
// server/modules/skill-fabric/src/core/compiler/CodeCompiler.ts
|
||||
// Compiles JavaScript/TypeScript-like skill code, validates syntax
|
||||
|
||||
import type { CompileInput, CompileResult, CompileError } from "../../types";
|
||||
|
||||
export class CodeCompiler {
|
||||
compile(input: CompileInput): CompileResult {
|
||||
const { source } = input;
|
||||
const errors: CompileError[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!source || source.trim().length === 0) {
|
||||
errors.push({ message: "Código não pode estar vazio", severity: "error", field: "source" } as CompileError & { field: string });
|
||||
return { ok: false, errors };
|
||||
}
|
||||
|
||||
// Basic syntax checks
|
||||
const syntaxErrors = this.checkSyntax(source);
|
||||
errors.push(...syntaxErrors);
|
||||
|
||||
if (syntaxErrors.some((e) => e.severity === "error")) {
|
||||
return { ok: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Prepend //code marker so SkillEntity can detect the mode
|
||||
const body = source.trimStart().startsWith("//code") ? source : `//code\n${source}`;
|
||||
|
||||
// Warn if no return statement
|
||||
if (!source.includes("return ")) {
|
||||
warnings.push("Nenhuma instrução 'return' encontrada — a skill não retornará dados");
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
body,
|
||||
errors: [],
|
||||
warnings,
|
||||
metadata: { mode: "code", linesOfCode: source.split("\n").length },
|
||||
};
|
||||
}
|
||||
|
||||
private checkSyntax(source: string): CompileError[] {
|
||||
const errors: CompileError[] = [];
|
||||
|
||||
// Check balanced braces
|
||||
let braces = 0;
|
||||
let parens = 0;
|
||||
const lines = source.split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
for (const ch of line) {
|
||||
if (ch === "{") braces++;
|
||||
if (ch === "}") braces--;
|
||||
if (ch === "(") parens++;
|
||||
if (ch === ")") parens--;
|
||||
}
|
||||
|
||||
if (braces < 0) {
|
||||
errors.push({ line: i + 1, message: "Chave de fechamento sem abertura correspondente", severity: "error" });
|
||||
braces = 0;
|
||||
}
|
||||
if (parens < 0) {
|
||||
errors.push({ line: i + 1, message: "Parêntese de fechamento sem abertura correspondente", severity: "error" });
|
||||
parens = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (braces > 0) {
|
||||
errors.push({ message: `${braces} chave(s) de abertura sem fechamento`, severity: "error" });
|
||||
}
|
||||
if (parens > 0) {
|
||||
errors.push({ message: `${parens} parêntese(s) de abertura sem fechamento`, severity: "error" });
|
||||
}
|
||||
|
||||
// Detect obviously dangerous patterns (Rule 2: security)
|
||||
const forbidden = [
|
||||
{ pattern: /require\s*\(/, msg: "require() não permitido em skills — use os helpers disponíveis no contexto" },
|
||||
{ pattern: /process\.exit/, msg: "process.exit() não permitido em skills" },
|
||||
{ pattern: /child_process/, msg: "child_process não permitido em skills" },
|
||||
{ pattern: /fs\./, msg: "Acesso direto ao filesystem não permitido em skills" },
|
||||
];
|
||||
|
||||
for (const { pattern, msg } of forbidden) {
|
||||
if (pattern.test(source)) {
|
||||
errors.push({ message: msg, severity: "error" });
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
export const codeCompiler = new CodeCompiler();
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
// server/modules/skill-fabric/src/core/compiler/MarkdownCompiler.ts
|
||||
// Validates and processes Markdown skill bodies (with optional YAML frontmatter)
|
||||
|
||||
import type { CompileInput, CompileResult, CompileError } from "../../types";
|
||||
|
||||
export class MarkdownCompiler {
|
||||
compile(input: CompileInput): CompileResult {
|
||||
const { source } = input;
|
||||
const errors: CompileError[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!source || source.trim().length === 0) {
|
||||
errors.push({ message: "Corpo Markdown não pode estar vazio", severity: "error" });
|
||||
return { ok: false, errors };
|
||||
}
|
||||
|
||||
// Parse optional YAML frontmatter
|
||||
let body = source;
|
||||
let metadata: Record<string, unknown> = { mode: "markdown" };
|
||||
|
||||
if (source.startsWith("---")) {
|
||||
const frontmatterResult = this.parseFrontmatter(source);
|
||||
if (frontmatterResult.error) {
|
||||
errors.push({ message: `Erro no frontmatter YAML: ${frontmatterResult.error}`, severity: "error" });
|
||||
return { ok: false, errors };
|
||||
}
|
||||
body = frontmatterResult.body;
|
||||
metadata = { ...metadata, frontmatter: frontmatterResult.data };
|
||||
}
|
||||
|
||||
// Check for empty body after frontmatter stripping
|
||||
if (body.trim().length === 0) {
|
||||
warnings.push("Corpo Markdown está vazio após o frontmatter — adicione instruções");
|
||||
}
|
||||
|
||||
// Validate template references format: {{parameters.x}}, {{/var/x}}
|
||||
const invalidRefs = this.findInvalidReferences(body);
|
||||
for (const ref of invalidRefs) {
|
||||
errors.push({ message: `Referência de template inválida: ${ref}`, severity: "error" });
|
||||
}
|
||||
|
||||
if (errors.some((e) => e.severity === "error")) {
|
||||
return { ok: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Count lines and detect structure
|
||||
const lines = body.split("\n");
|
||||
const headings = lines.filter((l) => l.startsWith("#")).length;
|
||||
if (headings === 0) {
|
||||
warnings.push("Nenhum título (# Heading) encontrado — considere estruturar com headings");
|
||||
}
|
||||
|
||||
metadata.linesOfContent = lines.length;
|
||||
metadata.headingsCount = headings;
|
||||
|
||||
return { ok: true, body: source, errors: [], warnings, metadata };
|
||||
}
|
||||
|
||||
private parseFrontmatter(source: string): { data: Record<string, unknown>; body: string; error?: string } {
|
||||
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||
if (!match) {
|
||||
return { data: {}, body: source, error: "Frontmatter malformado — certifique-se de fechar com '---'" };
|
||||
}
|
||||
|
||||
const yamlText = match[1];
|
||||
const body = match[2] ?? "";
|
||||
const data: Record<string, unknown> = {};
|
||||
|
||||
// Simple YAML key: value parser (covers common cases)
|
||||
try {
|
||||
for (const line of yamlText.split("\n")) {
|
||||
const colonIdx = line.indexOf(":");
|
||||
if (colonIdx === -1) continue;
|
||||
const key = line.slice(0, colonIdx).trim();
|
||||
const value = line.slice(colonIdx + 1).trim();
|
||||
if (key) {
|
||||
// Detect arrays: value starts with [ or subsequent lines start with -
|
||||
if (value.startsWith("[") && value.endsWith("]")) {
|
||||
data[key] = value
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map((s) => s.trim().replace(/^["']|["']$/g, ""));
|
||||
} else {
|
||||
data[key] = value.replace(/^["']|["']$/g, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return { data: {}, body, error: String(err) };
|
||||
}
|
||||
|
||||
return { data, body };
|
||||
}
|
||||
|
||||
private findInvalidReferences(body: string): string[] {
|
||||
const invalid: string[] = [];
|
||||
// Find {{ ... }} patterns and validate them
|
||||
const pattern = /\{\{([^}]+)\}\}/g;
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = pattern.exec(body)) !== null) {
|
||||
const inner = m[1].trim();
|
||||
const validPatterns = [
|
||||
/^parameters\.\w+(\.\w+)*$/,
|
||||
/^\/var\/[\w/]+(\.\w+)*$/,
|
||||
/^\/skill[\w/]*$/,
|
||||
/^\w+$/, // simple variable name
|
||||
];
|
||||
if (!validPatterns.some((p) => p.test(inner))) {
|
||||
invalid.push(m[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return invalid;
|
||||
}
|
||||
}
|
||||
|
||||
export const markdownCompiler = new MarkdownCompiler();
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// server/modules/skill-fabric/src/core/compiler/VisualCompiler.ts
|
||||
// Compiles visual node-graph skill definitions into a Markdown body
|
||||
// Visual canvas is a stretch goal — this stub handles the future format
|
||||
|
||||
import type { CompileInput, CompileResult } from "../../types";
|
||||
|
||||
export interface VisualNode {
|
||||
id: string;
|
||||
type: "trigger" | "step" | "condition" | "output";
|
||||
label: string;
|
||||
config?: Record<string, unknown>;
|
||||
next?: string[]; // node ids
|
||||
}
|
||||
|
||||
export interface VisualGraph {
|
||||
nodes: VisualNode[];
|
||||
edges: Array<{ from: string; to: string }>;
|
||||
}
|
||||
|
||||
export class VisualCompiler {
|
||||
compile(input: CompileInput): CompileResult {
|
||||
const { source } = input;
|
||||
|
||||
if (!source || source.trim().length === 0) {
|
||||
return { ok: false, errors: [{ message: "Grafo visual não pode estar vazio", severity: "error" }] };
|
||||
}
|
||||
|
||||
let graph: VisualGraph;
|
||||
try {
|
||||
graph = JSON.parse(source) as VisualGraph;
|
||||
} catch {
|
||||
return { ok: false, errors: [{ message: "Grafo visual inválido — esperado JSON", severity: "error" }] };
|
||||
}
|
||||
|
||||
if (!graph.nodes || graph.nodes.length === 0) {
|
||||
return { ok: false, errors: [{ message: "Grafo visual deve ter pelo menos um nó", severity: "error" }] };
|
||||
}
|
||||
|
||||
// Convert visual graph to Markdown body
|
||||
const body = this.graphToMarkdown(graph);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
body,
|
||||
errors: [],
|
||||
warnings: ["Modo visual está em fase experimental"],
|
||||
metadata: { mode: "visual", nodeCount: graph.nodes.length },
|
||||
};
|
||||
}
|
||||
|
||||
private graphToMarkdown(graph: VisualGraph): string {
|
||||
const lines: string[] = ["# Skill Visual\n"];
|
||||
|
||||
const trigger = graph.nodes.find((n) => n.type === "trigger");
|
||||
if (trigger) {
|
||||
lines.push(`## Gatilho: ${trigger.label}\n`);
|
||||
}
|
||||
|
||||
const steps = graph.nodes.filter((n) => n.type === "step");
|
||||
if (steps.length > 0) {
|
||||
lines.push("## Passos\n");
|
||||
for (const step of steps) {
|
||||
lines.push(`- ${step.label}`);
|
||||
if (step.config) {
|
||||
for (const [k, v] of Object.entries(step.config)) {
|
||||
lines.push(` - ${k}: ${String(v)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const outputs = graph.nodes.filter((n) => n.type === "output");
|
||||
if (outputs.length > 0) {
|
||||
lines.push("\n## Saída\n");
|
||||
for (const out of outputs) {
|
||||
lines.push(`- ${out.label}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
export const visualCompiler = new VisualCompiler();
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
// server/modules/skill-fabric/src/core/executor/Sandbox.ts
|
||||
// Safe code execution using Node.js built-in vm module (NOT vm2)
|
||||
|
||||
import vm from "vm";
|
||||
import type { SandboxOptions, SandboxResult } from "../../types";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 5000;
|
||||
|
||||
export class Sandbox {
|
||||
run(opts: SandboxOptions): SandboxResult {
|
||||
const logs: string[] = [];
|
||||
const timeout = opts.timeout ?? DEFAULT_TIMEOUT_MS;
|
||||
|
||||
// Build a safe context — only expose what we explicitly allow
|
||||
const context: Record<string, unknown> = {
|
||||
// Safe console (captures logs instead of printing them)
|
||||
console: {
|
||||
log: (...args: unknown[]) => logs.push(args.map(String).join(" ")),
|
||||
warn: (...args: unknown[]) => logs.push("[warn] " + args.map(String).join(" ")),
|
||||
error: (...args: unknown[]) => logs.push("[error] " + args.map(String).join(" ")),
|
||||
},
|
||||
// Safe Math, Date, JSON
|
||||
Math,
|
||||
Date,
|
||||
JSON,
|
||||
// Utility helpers
|
||||
String,
|
||||
Number,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
parseInt,
|
||||
parseFloat,
|
||||
isNaN,
|
||||
isFinite,
|
||||
encodeURIComponent,
|
||||
decodeURIComponent,
|
||||
// User-provided context variables
|
||||
...(opts.context ?? {}),
|
||||
};
|
||||
|
||||
vm.createContext(context);
|
||||
|
||||
try {
|
||||
// Wrap code in an async IIFE so top-level await is possible
|
||||
const wrappedCode = `
|
||||
(async function __skill__() {
|
||||
${opts.code}
|
||||
})()
|
||||
`;
|
||||
const script = new vm.Script(wrappedCode, { filename: "skill.js" });
|
||||
const result = script.runInContext(context, { timeout }) as Promise<unknown> | unknown;
|
||||
|
||||
// Handle both sync and async execution
|
||||
if (result instanceof Promise) {
|
||||
// For async, we use a synchronous trick via vm timers
|
||||
// Since vm doesn't natively support Promise.resolve waiting without event loop,
|
||||
// we return the promise wrapped result as "pending" — caller should use runAsync
|
||||
return { ok: true, value: "[async — use runAsync]", logs };
|
||||
}
|
||||
|
||||
return { ok: true, value: result, logs };
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
logs.push("[error] " + error);
|
||||
return { ok: false, error, logs };
|
||||
}
|
||||
}
|
||||
|
||||
/** Async-safe version that properly awaits the result */
|
||||
async runAsync(opts: SandboxOptions): Promise<SandboxResult> {
|
||||
const logs: string[] = [];
|
||||
const timeout = opts.timeout ?? DEFAULT_TIMEOUT_MS;
|
||||
|
||||
const context: Record<string, unknown> = {
|
||||
console: {
|
||||
log: (...args: unknown[]) => logs.push(args.map(String).join(" ")),
|
||||
warn: (...args: unknown[]) => logs.push("[warn] " + args.map(String).join(" ")),
|
||||
error: (...args: unknown[]) => logs.push("[error] " + args.map(String).join(" ")),
|
||||
},
|
||||
Math,
|
||||
Date,
|
||||
JSON,
|
||||
String,
|
||||
Number,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
parseInt,
|
||||
parseFloat,
|
||||
isNaN,
|
||||
isFinite,
|
||||
encodeURIComponent,
|
||||
decodeURIComponent,
|
||||
Promise,
|
||||
...(opts.context ?? {}),
|
||||
};
|
||||
|
||||
vm.createContext(context);
|
||||
|
||||
const wrappedCode = `
|
||||
(async function __skill__() {
|
||||
${opts.code}
|
||||
})()
|
||||
`;
|
||||
|
||||
try {
|
||||
const script = new vm.Script(wrappedCode, { filename: "skill.js" });
|
||||
|
||||
// Run the script (returns a Promise from the IIFE)
|
||||
const promise = script.runInContext(context) as Promise<unknown>;
|
||||
|
||||
// Race against timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Timeout: execução excedeu ${timeout}ms`)), timeout)
|
||||
);
|
||||
|
||||
const value = await Promise.race([promise, timeoutPromise]);
|
||||
return { ok: true, value, logs };
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
logs.push("[error] " + error);
|
||||
return { ok: false, error, logs };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sandbox = new Sandbox();
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
// server/modules/skill-fabric/src/core/executor/SkillExecutor.ts
|
||||
// Executes skills — dispatches to Sandbox (code) or template renderer (markdown)
|
||||
|
||||
import { db } from "../../../../../../db";
|
||||
import { skills, skillExecutions } from "@shared/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import crypto from "crypto";
|
||||
import { sandbox } from "./Sandbox";
|
||||
import type { FabricExecuteOptions, FabricExecuteResult } from "../../types";
|
||||
|
||||
export class SkillExecutor {
|
||||
async execute(opts: FabricExecuteOptions): Promise<FabricExecuteResult> {
|
||||
const start = Date.now();
|
||||
|
||||
// 1. Load skill
|
||||
const conditions = [eq(skills.id, opts.skillId)];
|
||||
if (opts.tenantId) conditions.push(eq(skills.tenantId, opts.tenantId));
|
||||
|
||||
const [skill] = await db.select().from(skills).where(and(...conditions)).limit(1);
|
||||
if (!skill) {
|
||||
return { ok: false, error: `Skill não encontrada: ${opts.skillId}`, durationMs: 0, executionId: "" };
|
||||
}
|
||||
|
||||
if (skill.status !== "active") {
|
||||
return { ok: false, error: `Skill está ${skill.status} — somente skills ativas podem ser executadas`, durationMs: 0, executionId: "" };
|
||||
}
|
||||
|
||||
// 2. Create execution record
|
||||
const [execution] = await db.insert(skillExecutions).values({
|
||||
skillId: skill.id,
|
||||
tenantId: opts.tenantId ?? null,
|
||||
triggeredBy: "manual",
|
||||
triggeredByUserId: opts.userId ?? null,
|
||||
parameters: opts.parameters ?? {},
|
||||
status: "running",
|
||||
}).returning();
|
||||
|
||||
const logs: string[] = [];
|
||||
|
||||
try {
|
||||
let result: unknown;
|
||||
|
||||
const body = skill.body ?? "";
|
||||
const isCodeSkill = body.trimStart().startsWith("//code");
|
||||
|
||||
if (isCodeSkill) {
|
||||
// Strip the //code marker before executing
|
||||
const code = body.replace(/^\/\/code\s*\n?/, "");
|
||||
const sandboxResult = await sandbox.runAsync({
|
||||
code,
|
||||
context: {
|
||||
parameters: opts.parameters ?? {},
|
||||
skillId: skill.id,
|
||||
skillSlug: skill.slug,
|
||||
},
|
||||
timeout: opts.timeout ?? 5000,
|
||||
});
|
||||
|
||||
logs.push(...sandboxResult.logs);
|
||||
|
||||
if (!sandboxResult.ok) {
|
||||
throw new Error(sandboxResult.error ?? "Erro de execução no sandbox");
|
||||
}
|
||||
result = sandboxResult.value;
|
||||
} else {
|
||||
// Markdown skill — interpolate template variables
|
||||
result = this.renderMarkdown(body, opts.parameters ?? {});
|
||||
}
|
||||
|
||||
const durationMs = Date.now() - start;
|
||||
const auditHash = crypto.createHash("sha256").update(JSON.stringify(result)).digest("hex");
|
||||
|
||||
await db.update(skillExecutions)
|
||||
.set({ status: "success", result: result as Record<string, unknown>, durationMs, completedAt: new Date(), auditHash })
|
||||
.where(eq(skillExecutions.id, execution.id));
|
||||
|
||||
return { ok: true, data: result, durationMs, executionId: execution.id, logs };
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
const durationMs = Date.now() - start;
|
||||
|
||||
await db.update(skillExecutions)
|
||||
.set({ status: "failed", error, durationMs, completedAt: new Date() })
|
||||
.where(eq(skillExecutions.id, execution.id));
|
||||
|
||||
return { ok: false, error, durationMs, executionId: execution.id, logs };
|
||||
}
|
||||
}
|
||||
|
||||
private renderMarkdown(body: string, parameters: Record<string, unknown>): unknown {
|
||||
let output = body;
|
||||
|
||||
// Substitute {{parameters.x}}
|
||||
output = output.replace(/\{\{parameters\.([^}]+)\}\}/g, (_, k: string) => {
|
||||
const val = (parameters as Record<string, unknown>)[k];
|
||||
return val !== undefined ? String(val) : "";
|
||||
});
|
||||
|
||||
// Substitute {{varName}} — simple top-level vars from parameters
|
||||
output = output.replace(/\{\{(\w+)\}\}/g, (_, k: string) => {
|
||||
const val = (parameters as Record<string, unknown>)[k];
|
||||
return val !== undefined ? String(val) : `{{${k}}}`;
|
||||
});
|
||||
|
||||
return { rendered: output, renderedAt: new Date().toISOString() };
|
||||
}
|
||||
}
|
||||
|
||||
export const skillExecutor = new SkillExecutor();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue