diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2aefa82..ca95410 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -7,8 +7,8 @@ Evolução do Arcádia Suite de ERP tradicional para Sistema Agêntico Orientado ## Phases - [x] **Phase 1: Fundação** - Infraestrutura base: submodules MiroFlow/OpenClaw, tabelas skills, Neo4j, ReferenceParser -- [~] **Phase 2: Skills Engine** - Skills criáveis e executáveis com editor Monaco e Marketplace -- [ ] **Phase 3: MiroFlow Embutido** - Análises científicas via agentes especializados + bridge Superset +- [x] **Phase 2: Skills Engine** - Skills criáveis e executáveis com editor Monaco e Marketplace +- [x] **Phase 3: MiroFlow Embutido** - Análises científicas via agentes especializados + bridge Superset - [ ] **Phase 4: OpenClaw Embutido** - Skills emergentes com detecção de padrões - [ ] **Phase 5: Automation Fabric** - Automações unificadas (XOS + Central) - [ ] **Phase 6: Dev Center Completo** - Fábrica de agentes: Design → Assemble → Deploy @@ -43,7 +43,7 @@ Plans: - [x] 02-01: SkillEngine + API REST - [x] 02-02: Editor Monaco + autocomplete + rota /skills - [x] 02-03: Skill Marketplace (Biblioteca) -- [ ] 02-04: Versionamento Git-like de skills +- [x] 02-04: Versionamento Git-like de skills ### Phase 3: MiroFlow Embutido **Goal**: Análises científicas disponíveis via agentes especializados integrados ao Superset @@ -62,7 +62,7 @@ Plans: Plans: - [x] 03-01-PLAN.md — Setup (ollama pull llama3.1:8b) + miroflow_service.py FastAPI porta 8006 com 3 agentes - [x] 03-02-PLAN.md — Node bridge (engine-proxy.ts + routes.ts) + KG logging SHA-256 -- [ ] 03-03-PLAN.md — Frontend MiroFlowControl.tsx + tab "Científico" em BiWorkspace.tsx +- [x] 03-03-PLAN.md — Frontend MiroFlowControl.tsx + tab "Científico" em BiWorkspace.tsx ### Phase 4: OpenClaw Embutido **Goal**: Skills emergentes criadas automaticamente a partir de padrões detectados @@ -99,8 +99,18 @@ Plans: | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 1. Fundação | 3/3 | Complete | 2026-03-25 | -| 2. Skills Engine | 3/4 | In progress | - | -| 3. MiroFlow Embutido | 2/3 | In Progress| | +| 2. Skills Engine | 4/4 | Complete | 2026-03-26 | +| 3. MiroFlow Embutido | 3/3 | Complete | 2026-03-26 | | 4. OpenClaw Embutido | 0/TBD | Not started | - | | 5. Automation Fabric | 0/TBD | Not started | - | | 6. Dev Center Completo | 0/TBD | Not started | - | + +### Phase 7: Skill Fabric Expandido: Compiladores, Sandbox Executor, Visual/Code/Markdown Editors com Validation Pipeline + +**Goal:** [To be planned] +**Requirements**: TBD +**Depends on:** Phase 6 +**Plans:** 0 plans + +Plans: +- [ ] TBD (run /gsd:plan-phase 7 to break down) diff --git a/.planning/STATE.md b/.planning/STATE.md index c5b84a9..7e3ae49 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,16 +1,17 @@ # State -## Current Phase: 3 — MiroFlow Embutido +## Current Phase: 4 — OpenClaw Embutido ## Completed - Phase 1: submodules, tabelas, Neo4j, ReferenceParser -- Phase 2: SkillEngine, API REST, Monaco Editor, /skills, autocomplete, Marketplace - -## Completed Plans -- Phase 3, Plan 01: miroflow_service.py + tests (60f1c5c, 76e1d34) +- Phase 2: SkillEngine, API REST, Monaco Editor, /skills, autocomplete, Marketplace, versionamento Git-like +- Phase 3: miroflow_service.py, bridge TS + KG logging, MiroFlowControl.tsx + tab Científico ## In Progress -- Phase 2 pendente: versionamento Git-like de skills (pode ser feito em paralelo ou movido para backlog) +- (nenhum — iniciar Phase 4) + +## Roadmap Evolution +- Phase 7 added: Skill Fabric Expandido (compiladores, sandbox, 3 editor modes, validation pipeline) ## Notes - Superset em produção com RLS configurado — não alterar sem confirmação diff --git a/.planning/phases/04-openclaw-embutido/04-CONTEXT.md b/.planning/phases/04-openclaw-embutido/04-CONTEXT.md new file mode 100644 index 0000000..3cfb45b --- /dev/null +++ b/.planning/phases/04-openclaw-embutido/04-CONTEXT.md @@ -0,0 +1,97 @@ +# Phase 4 Context — OpenClaw Embutido + +> Generated in --auto mode on 2026-03-26. All decisions auto-selected with recommended defaults. + +## Phase Goal + +Skills emergentes criadas automaticamente a partir de padrões detectados em `skill_executions`. + +## Prior Context Applied + +- **Stack de IA:** Ollama local via LiteLLM — usar `llama3.1:8b` para geração de drafts (leve, rápido) +- **Backend-first:** API definida antes do frontend +- **Auditoria imutável:** todas as execuções registradas com SHA-256 +- **Schema já existe:** `detected_patterns` e `skill_suggestions` em `shared/schema.ts` — usar sem alterar + +--- + +## Decisions + +### 1. O que constitui um "padrão"? + +**[auto] Mesma skill executada ≥3 vezes em 30 dias com confiança ≥80%** + +- Fonte de dados: tabela `skill_executions` +- Agrupamento: `skillId + userId` (por usuário, não global) +- Confiança calculada como: `(frequency / max_frequency_in_window) * 100` +- Janela: `lastSeenAt - firstSeenAt ≤ 30 dias` +- Thresholds alinhados com o roadmap: min 3 ocorrências, 30 dias, 80% confiança + +### 2. Trigger do PatternDetector + +**[auto] Cron job a cada hora (setInterval no servidor Node.js)** + +- Sem dependência de infraestrutura externa (sem Redis, sem Bull) +- Ao iniciar, PatternDetector registra no startup do servidor +- Execuções recentes analisadas em lote; padrões gravados em `detected_patterns` +- Se padrão já existe (mesmo skillId + userId): atualiza frequency + lastSeenAt + +### 3. Geração do body da skill emergente + +**[auto] AI via llama3.1:8b (LiteLLM) gera o body do DRAFT** + +- Ao atingir threshold: PatternDetector cria entrada em `skill_suggestions` (status: `pending`) +- Skill DRAFT gerada automaticamente em `arcadia_skills` (status: `draft`, source: `openclaw`) +- Body gerado via prompt para llama3.1:8b descrevendo o padrão detectado +- DRAFT aguarda aprovação — não é executável até ser `published` + +### 4. Widget de notificação + +**[auto] Badge no header global + painel slide-in** + +- Badge no ícone de notificações existente no header (sem criar novo componente de header) +- Ao clicar: slide-in panel lateral mostrando lista de sugestões pendentes +- Cada sugestão mostra: nome sugerido, padrão detectado, frequência, confiança, body preview +- Ações inline: "Aceitar" → promove para skill publicada | "Rejeitar" → status `rejected` + +### 5. Aprovação — onde? + +**[auto] Tab "Sugestões" em `/skills` (página existente) — Dev Center completo na Phase 6** + +- Sem criar nova rota — adicionar tab na página `/skills` já existente +- Tab lista `skill_suggestions` com status `pending` +- Fluxo: aceitar → cria skill publicada a partir do DRAFT | rejeitar → arquiva sugestão +- Dev Center completo (Phase 6) herdará esse fluxo + +--- + +## Reusable Assets Identified + +- `shared/schema.ts` — `detectedPatterns`, `skillSuggestions` já definidos (Phase 1) +- `server/skills/engine.ts` — SkillEngine para criar DRAFT skills programaticamente +- `server/skills/routes.ts` — rota `/api/skills` existente, adicionar sub-rotas de sugestões +- `server/skills/versioning.ts` — SHA-256 audit logging (reusar padrão) +- `client/src/pages/BiWorkspace.tsx` — referência de como adicionar tabs a uma página existente +- `docker/litellm-config.yaml` — llama3.1:8b já configurado como Tier 2 + +--- + +## Scope Boundary + +**Incluído nesta fase:** +- PatternDetector backend (cron + análise de skill_executions) +- Geração de DRAFT via AI +- Widget de notificação + slide-in +- Tab "Sugestões" em /skills com aprovação/rejeição + +**Fora de escopo (phases futuras):** +- Dev Center completo (Phase 6) +- Detecção de padrões em outras fontes além de skill_executions +- Pattern sharing entre tenants + +--- + +## Plan Breakdown Suggested + +- **04-01:** PatternDetector service (backend) — cron, análise, gravação em detected_patterns + skill_suggestions + DRAFT creation via AI +- **04-02:** API routes + frontend widget (badge + slide-in) + tab "Sugestões" em /skills diff --git a/client/src/components/OpenClawPanel.tsx b/client/src/components/OpenClawPanel.tsx new file mode 100644 index 0000000..1c63d01 --- /dev/null +++ b/client/src/components/OpenClawPanel.tsx @@ -0,0 +1,245 @@ +/** + * OpenClawPanel — Phase 4 + * + * Painel de visualização de Skills Emergentes detectadas pelo PatternDetector. + * Exibido como aba na sidebar do /development. + */ + +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + Sparkles, CheckCircle, XCircle, ChevronDown, ChevronRight, + Loader2, Activity, Clock, BarChart2 +} from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useToast } from "@/hooks/use-toast"; + +interface Suggestion { + id: string; + suggestedSkillName: string; + suggestedDescription: string | null; + estimatedAutomation: string | null; + confidence: string; + draftBody: string | null; + createdAt: string; +} + +interface Pattern { + id: string; + actionType: string; + description: string | null; + frequency: number; + confidence: string; + firstSeenAt: string; + lastSeenAt: string; + status: string; +} + +export default function OpenClawPanel() { + const [tab, setTab] = useState<"suggestions" | "patterns">("suggestions"); + const [expanded, setExpanded] = useState(null); + const { toast } = useToast(); + const qc = useQueryClient(); + + const { data: sugData, isLoading: sugLoading } = useQuery<{ suggestions: Suggestion[]; total: number }>({ + queryKey: ["/api/openclaw/suggestions"], + refetchInterval: 2 * 60 * 1000, + }); + + const { data: patData, isLoading: patLoading } = useQuery<{ patterns: Pattern[]; total: number }>({ + queryKey: ["/api/openclaw/patterns"], + enabled: tab === "patterns", + }); + + const accept = useMutation({ + mutationFn: async (id: string) => { + const res = await fetch(`/api/openclaw/suggestions/${id}/accept`, { method: "POST" }); + if (!res.ok) throw new Error(); + return res.json(); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["/api/openclaw/suggestions"] }); + qc.invalidateQueries({ queryKey: ["/api/skills"] }); + toast({ title: "Skill publicada!" }); + }, + onError: () => toast({ title: "Erro ao aceitar", variant: "destructive" }), + }); + + const reject = useMutation({ + mutationFn: async (id: string) => { + const res = await fetch(`/api/openclaw/suggestions/${id}/reject`, { method: "POST" }); + if (!res.ok) throw new Error(); + return res.json(); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["/api/openclaw/suggestions"] }); + toast({ title: "Sugestão rejeitada" }); + }, + onError: () => toast({ title: "Erro ao rejeitar", variant: "destructive" }), + }); + + const suggestions = sugData?.suggestions ?? []; + const patterns = patData?.patterns ?? []; + + return ( +
+ {/* Header */} +
+ +
+

Skills Emergentes

+

OpenClaw — detecção automática de padrões

+
+
+ + {/* Tabs */} +
+ + +
+ + + {/* Sugestões */} + {tab === "suggestions" && ( +
+ {sugLoading && ( +
+ +
+ )} + {!sugLoading && suggestions.length === 0 && ( +
+ +

Nenhuma sugestão pendente

+

O detector roda a cada hora

+
+ )} +
+ {suggestions.map((s) => { + const conf = Math.round(Number(s.confidence) * 100); + const isExp = expanded === s.id; + const isAccepting = accept.isPending && accept.variables === s.id; + const isRejecting = reject.isPending && reject.variables === s.id; + + return ( +
+
+
+

{s.suggestedSkillName}

+ {s.suggestedDescription && ( +

{s.suggestedDescription}

+ )} +
+ + {conf}% + +
+ + {s.draftBody && ( +
+ + {isExp && ( +
+                            {s.draftBody}
+                          
+ )} +
+ )} + +
+ + +
+
+ ); + })} +
+
+ )} + + {/* Padrões */} + {tab === "patterns" && ( +
+ {patLoading && ( +
+ +
+ )} + {!patLoading && patterns.length === 0 && ( +
+ +

Nenhum padrão detectado

+

Execute skills manualmente para gerar dados

+
+ )} +
+ {patterns.map((p) => { + const conf = Math.round(Number(p.confidence) * 100); + const last = new Date(p.lastSeenAt).toLocaleDateString("pt-BR"); + return ( +
+
+

{p.actionType}

+ + {conf}% + +
+ {p.description && ( +

{p.description}

+ )} +
+ {p.frequency}x + {last} +
+
+ ); + })} +
+
+ )} +
+
+ ); +} diff --git a/client/src/pages/DevelopmentModule.tsx b/client/src/pages/DevelopmentModule.tsx index 38229ad..36ef143 100644 --- a/client/src/pages/DevelopmentModule.tsx +++ b/client/src/pages/DevelopmentModule.tsx @@ -25,8 +25,9 @@ import PageBuilder from "./PageBuilder"; import WorkflowBuilder from "./WorkflowBuilder"; import IDE from "./IDE"; import DevAgent from "@/components/lowcode/DevAgent"; +import OpenClawPanel from "@/components/OpenClawPanel"; -type ActiveTool = "home" | "doctypes" | "pages" | "workflows" | "dashboards" | "reports" | "scripts" | "ide" | "agent"; +type ActiveTool = "home" | "doctypes" | "pages" | "workflows" | "dashboards" | "reports" | "scripts" | "ide" | "agent" | "openclaw"; interface Dashboard { id: number; @@ -250,15 +251,24 @@ export default function DevelopmentModule() { count: 0, category: "dev" }, - { - id: "agent" as ActiveTool, - name: "Manus AI", + { + id: "agent" as ActiveTool, + name: "Manus AI", description: "Agente autônomo de IA", - icon: Bot, + icon: Bot, color: "bg-violet-600", count: 0, category: "dev" }, + { + id: "openclaw" as ActiveTool, + name: "Skills Emergentes", + description: "Padrões detectados automaticamente", + icon: Sparkles, + color: "bg-yellow-500", + count: 0, + category: "dev" + }, ]; const [, navigate] = useLocation(); @@ -705,6 +715,7 @@ export default function DevelopmentModule() { {activeTool === "scripts" && renderScriptEditor()} {activeTool === "ide" && } {activeTool === "agent" && } + {activeTool === "openclaw" && } diff --git a/migrations/phase4_openclaw_patterns.sql b/migrations/phase4_openclaw_patterns.sql new file mode 100644 index 0000000..42a9342 --- /dev/null +++ b/migrations/phase4_openclaw_patterns.sql @@ -0,0 +1,41 @@ +-- Phase 4: OpenClaw — Tabelas de detecção de padrões e sugestões de skills emergentes +-- Executar se as tabelas ainda não existirem (schema já definido em shared/schema.ts) + +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), + action_type VARCHAR(100) NOT NULL, + description TEXT, + frequency INTEGER NOT NULL DEFAULT 0, + confidence NUMERIC(4,3) NOT NULL DEFAULT 0, + first_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + metadata JSONB DEFAULT '{}', + status VARCHAR(20) NOT NULL DEFAULT 'active', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +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), + suggested_skill_name VARCHAR(200) NOT NULL, + suggested_description TEXT, + estimated_automation TEXT, + confidence NUMERIC(4,3) NOT NULL DEFAULT 0, + generated_skill_id UUID REFERENCES arcadia_skills(id), + status VARCHAR(20) NOT NULL DEFAULT 'pending', + 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 idx_detected_patterns_status ON detected_patterns(status); +CREATE INDEX IF NOT EXISTS idx_detected_patterns_user ON detected_patterns(user_id, action_type); +CREATE INDEX IF NOT EXISTS idx_skill_suggestions_status ON skill_suggestions(status); +CREATE INDEX IF NOT EXISTS idx_skill_suggestions_pattern ON skill_suggestions(pattern_id); diff --git a/server/openclaw/pattern-detector.ts b/server/openclaw/pattern-detector.ts new file mode 100644 index 0000000..7fe6c14 --- /dev/null +++ b/server/openclaw/pattern-detector.ts @@ -0,0 +1,223 @@ +/** + * PatternDetector — OpenClaw Phase 4 + * + * Analisa skill_executions a cada hora, detecta padrões por (skillId + userId), + * e gera DRAFT skills emergentes via llama3.1:8b quando threshold atingido. + * + * Thresholds: min 3 execuções, janela 30 dias, confiança ≥ 0.80 + */ + +import crypto from "crypto"; +import OpenAI from "openai"; +import { db } from "../../db/index"; +import { + arcadiaSkills, + skillExecutions, + detectedPatterns, + skillSuggestions, +} from "@shared/schema"; +import { eq, and, gte, sql, count } from "drizzle-orm"; + +const MIN_FREQUENCY = 3; +const WINDOW_DAYS = 30; +const MIN_CONFIDENCE = 0.80; +const CRON_INTERVAL_MS = 60 * 60 * 1000; // 1 hora + +const openai = new OpenAI({ + apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY, + baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL, +}); + +// ─── Análise de padrões ──────────────────────────────────────────────────────── + +async function detectPatterns(): Promise { + const windowStart = new Date(Date.now() - WINDOW_DAYS * 24 * 60 * 60 * 1000); + + // Agrupa execuções bem-sucedidas por skillId + userId na janela + const grouped = await db + .select({ + skillId: skillExecutions.skillId, + userId: skillExecutions.userId, + tenantId: skillExecutions.tenantId, + execCount: count(skillExecutions.id), + firstSeen: sql`MIN(${skillExecutions.startedAt})`, + lastSeen: sql`MAX(${skillExecutions.startedAt})`, + }) + .from(skillExecutions) + .where( + and( + gte(skillExecutions.startedAt, windowStart), + eq(skillExecutions.status, "success") + ) + ) + .groupBy(skillExecutions.skillId, skillExecutions.userId, skillExecutions.tenantId); + + // Frequência máxima no período (para normalizar confiança) + const maxFreq = grouped.reduce((m, r) => Math.max(m, Number(r.execCount)), 1); + + for (const row of grouped) { + const frequency = Number(row.execCount); + if (frequency < MIN_FREQUENCY) continue; + + const confidence = Math.min(frequency / maxFreq, 1); + if (confidence < MIN_CONFIDENCE) continue; + + // Busca skill para obter nome + const [skill] = await db + .select({ name: arcadiaSkills.name, slug: arcadiaSkills.slug, description: arcadiaSkills.description }) + .from(arcadiaSkills) + .where(eq(arcadiaSkills.id, row.skillId)) + .limit(1); + + if (!skill) continue; + + const actionType = `skill:${skill.slug}`; + + // Upsert em detected_patterns + const existing = await db + .select({ id: detectedPatterns.id }) + .from(detectedPatterns) + .where( + and( + eq(detectedPatterns.actionType, actionType), + eq(detectedPatterns.userId, row.userId ?? "") + ) + ) + .limit(1); + + let patternId: string; + + if (existing.length > 0) { + patternId = existing[0].id; + await db + .update(detectedPatterns) + .set({ + frequency, + confidence: String(confidence.toFixed(3)), + lastSeenAt: row.lastSeen, + updatedAt: new Date(), + }) + .where(eq(detectedPatterns.id, patternId)); + } else { + const [created] = await db + .insert(detectedPatterns) + .values({ + tenantId: row.tenantId, + userId: row.userId, + actionType, + description: `Skill "${skill.name}" executada repetidamente`, + frequency, + confidence: String(confidence.toFixed(3)), + firstSeenAt: row.firstSeen, + lastSeenAt: row.lastSeen, + metadata: { skillId: row.skillId, skillName: skill.name }, + status: "active", + }) + .returning({ id: detectedPatterns.id }); + patternId = created.id; + } + + // Cria sugestão se ainda não existe sugestão pendente para este padrão + const existingSuggestion = await db + .select({ id: skillSuggestions.id }) + .from(skillSuggestions) + .where( + and( + eq(skillSuggestions.patternId, patternId), + eq(skillSuggestions.status, "pending") + ) + ) + .limit(1); + + if (existingSuggestion.length === 0) { + await createEmergentSkillDraft(patternId, skill, frequency, confidence, row.tenantId, row.userId); + } + } +} + +// ─── Geração de DRAFT via AI ─────────────────────────────────────────────────── + +async function createEmergentSkillDraft( + patternId: string, + skill: { name: string; slug: string; description: string | null }, + frequency: number, + confidence: number, + tenantId: number | null, + userId: string | null +): Promise { + const prompt = `Você é um assistente de automação. Um usuário executou a skill "${skill.name}" (${skill.description ?? "sem descrição"}) ${frequency} vezes nos últimos 30 dias. + +Gere um corpo (body) em Markdown para uma nova skill chamada "Auto: ${skill.name}" que automatize este fluxo de forma mais inteligente. +O body deve usar blocos /skill/${skill.slug} para reutilizar a skill original e adicionar lógica de orquestração. +Responda apenas com o body em Markdown, sem explicações.`; + + let body = `# Auto: ${skill.name}\n\nEsta skill emergiu de ${frequency} execuções detectadas.\n\n\`\`\`\n/skill/${skill.slug}\n\`\`\`\n`; + + try { + const completion = await openai.chat.completions.create({ + model: "llama3.1:8b", + messages: [{ role: "user", content: prompt }], + max_tokens: 512, + temperature: 0.4, + }); + const generated = completion.choices[0]?.message?.content?.trim(); + if (generated) body = generated; + } catch { + // AI indisponível — usa body padrão + } + + // Cria DRAFT skill + const draftSlug = `auto-${skill.slug}-${crypto.randomBytes(4).toString("hex")}`; + const [draftSkill] = await db + .insert(arcadiaSkills) + .values({ + name: `Auto: ${skill.name}`, + slug: draftSlug, + description: `Skill emergente detectada automaticamente. Executada ${frequency} vezes com confiança ${(confidence * 100).toFixed(0)}%.`, + body, + status: "draft", + namespace: "tenant", + tenantId, + userId, + tags: ["emergente", "openclaw"], + author: "openclaw", + }) + .returning({ id: arcadiaSkills.id }); + + // Registra sugestão + await db.insert(skillSuggestions).values({ + patternId, + tenantId, + userId, + suggestedSkillName: `Auto: ${skill.name}`, + suggestedDescription: `Automatização detectada com base em ${frequency} execuções.`, + estimatedAutomation: body, + confidence: String(confidence.toFixed(3)), + generatedSkillId: draftSkill.id, + status: "pending", + source: "openclaw", + }); +} + +// ─── Inicialização do cron ───────────────────────────────────────────────────── + +let cronHandle: ReturnType | null = null; + +export function startPatternDetector(): void { + if (cronHandle) return; + + // Executa imediatamente na inicialização, depois a cada hora + detectPatterns().catch(console.error); + cronHandle = setInterval(() => { + detectPatterns().catch(console.error); + }, CRON_INTERVAL_MS); + + console.log("[OpenClaw] PatternDetector iniciado (intervalo: 1h)"); +} + +export function stopPatternDetector(): void { + if (cronHandle) { + clearInterval(cronHandle); + cronHandle = null; + } +} diff --git a/server/openclaw/routes.ts b/server/openclaw/routes.ts new file mode 100644 index 0000000..07a3a8f --- /dev/null +++ b/server/openclaw/routes.ts @@ -0,0 +1,149 @@ +/** + * OpenClaw Routes — Fase 4 + * + * GET /api/openclaw/suggestions — lista sugestões pendentes + * POST /api/openclaw/suggestions/:id/accept — aceita sugestão (publica DRAFT) + * POST /api/openclaw/suggestions/:id/reject — rejeita sugestão + * GET /api/openclaw/patterns — lista padrões detectados + */ + +import type { Express, Request, Response } from "express"; +import { db } from "../../db/index"; +import { arcadiaSkills, detectedPatterns, skillSuggestions } from "@shared/schema"; +import { eq, and, desc } from "drizzle-orm"; + +export function registerOpenClawRoutes(app: Express): void { + + // ── Listar sugestões pendentes ────────────────────────────────────────────── + app.get("/api/openclaw/suggestions", async (req: Request, res: Response) => { + try { + const suggestions = await db + .select() + .from(skillSuggestions) + .where(eq(skillSuggestions.status, "pending")) + .orderBy(desc(skillSuggestions.createdAt)) + .limit(50); + + // Enriquecer com body do DRAFT + const enriched = await Promise.all( + suggestions.map(async (s) => { + let draftBody: string | null = null; + if (s.generatedSkillId) { + const [draft] = await db + .select({ body: arcadiaSkills.body, name: arcadiaSkills.name }) + .from(arcadiaSkills) + .where(eq(arcadiaSkills.id, s.generatedSkillId)) + .limit(1); + draftBody = draft?.body ?? null; + } + return { ...s, draftBody }; + }) + ); + + res.json({ suggestions: enriched, total: enriched.length }); + } catch (err) { + res.status(500).json({ error: "Erro ao listar sugestões" }); + } + }); + + // ── Aceitar sugestão ──────────────────────────────────────────────────────── + app.post("/api/openclaw/suggestions/:id/accept", async (req: Request, res: Response) => { + const { id } = req.params; + const userId = (req as any).user?.id ?? null; + + try { + const [suggestion] = await db + .select() + .from(skillSuggestions) + .where(and(eq(skillSuggestions.id, id), eq(skillSuggestions.status, "pending"))) + .limit(1); + + if (!suggestion) { + return res.status(404).json({ error: "Sugestão não encontrada" }); + } + + // Publica o DRAFT + if (suggestion.generatedSkillId) { + await db + .update(arcadiaSkills) + .set({ status: "active", updatedAt: new Date() }) + .where(eq(arcadiaSkills.id, suggestion.generatedSkillId)); + } + + // Atualiza sugestão e padrão + await db + .update(skillSuggestions) + .set({ status: "accepted", reviewedBy: userId, reviewedAt: new Date(), updatedAt: new Date() }) + .where(eq(skillSuggestions.id, id)); + + if (suggestion.patternId) { + await db + .update(detectedPatterns) + .set({ status: "converted", updatedAt: new Date() }) + .where(eq(detectedPatterns.id, suggestion.patternId)); + } + + res.json({ ok: true, skillId: suggestion.generatedSkillId }); + } catch (err) { + res.status(500).json({ error: "Erro ao aceitar sugestão" }); + } + }); + + // ── Rejeitar sugestão ─────────────────────────────────────────────────────── + app.post("/api/openclaw/suggestions/:id/reject", async (req: Request, res: Response) => { + const { id } = req.params; + const userId = (req as any).user?.id ?? null; + + try { + const [suggestion] = await db + .select() + .from(skillSuggestions) + .where(and(eq(skillSuggestions.id, id), eq(skillSuggestions.status, "pending"))) + .limit(1); + + if (!suggestion) { + return res.status(404).json({ error: "Sugestão não encontrada" }); + } + + // Remove o DRAFT + if (suggestion.generatedSkillId) { + await db + .update(arcadiaSkills) + .set({ status: "archived", updatedAt: new Date() }) + .where(eq(arcadiaSkills.id, suggestion.generatedSkillId)); + } + + await db + .update(skillSuggestions) + .set({ status: "rejected", reviewedBy: userId, reviewedAt: new Date(), updatedAt: new Date() }) + .where(eq(skillSuggestions.id, id)); + + if (suggestion.patternId) { + await db + .update(detectedPatterns) + .set({ status: "archived", updatedAt: new Date() }) + .where(eq(detectedPatterns.id, suggestion.patternId)); + } + + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: "Erro ao rejeitar sugestão" }); + } + }); + + // ── Listar padrões detectados ─────────────────────────────────────────────── + app.get("/api/openclaw/patterns", async (_req: Request, res: Response) => { + try { + const patterns = await db + .select() + .from(detectedPatterns) + .where(eq(detectedPatterns.status, "active")) + .orderBy(desc(detectedPatterns.updatedAt)) + .limit(100); + + res.json({ patterns, total: patterns.length }); + } catch (err) { + res.status(500).json({ error: "Erro ao listar padrões" }); + } + }); +} diff --git a/server/routes.ts b/server/routes.ts index 41e6202..7a4fa82 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -65,6 +65,8 @@ import { loadModuleRoutes } from "./modules/loader"; import graphRoutes from "./graph/routes"; import { initSocketIO } from "./socket-io"; import { startXosScheduler } from "./xos/scheduler"; +import { registerOpenClawRoutes } from "./openclaw/routes"; +import { startPatternDetector } from "./openclaw/pattern-detector"; export async function registerRoutes( httpServer: Server, @@ -160,6 +162,10 @@ export async function registerRoutes( // XOS Scheduler: SLA breach checker + supervisor stats broadcaster startXosScheduler(); + + // OpenClaw — PatternDetector (Phase 4) + registerOpenClawRoutes(app); + startPatternDetector(); // Central de Protocolos (MCP, A2A, AP2, UCP) app.use("/api", protocolsRoutes); diff --git a/server/skills/versioning.ts b/server/skills/versioning.ts index 64f1657..679c4e4 100644 --- a/server/skills/versioning.ts +++ b/server/skills/versioning.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { db } from '@/lib/db'; +import { db } from '../../db/index'; import { Skill } from './engine'; export interface SkillVersion {