feat(04): OpenClaw Embutido — PatternDetector + Skills Emergentes
- PatternDetector (cron horário): analisa skill_executions, detecta padrões (≥3x, 30 dias, ≥80% confiança), grava em detected_patterns - Geração de DRAFT skills via llama3.1:8b ao atingir threshold - API routes: GET /api/openclaw/suggestions, POST accept/reject, GET /api/openclaw/patterns - OpenClawPanel.tsx: painel de visualização com tabs Sugestões/Padrões - Aba "Skills Emergentes" (ícone Sparkles) na sidebar do /development - Migration SQL para detected_patterns + skill_suggestions - fix: corrige import @/lib/db → ../../db/index em versioning.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7528d4c36e
commit
c73b9d6658
|
|
@ -7,8 +7,8 @@ Evolução do Arcádia Suite de ERP tradicional para Sistema Agêntico Orientado
|
||||||
## Phases
|
## Phases
|
||||||
|
|
||||||
- [x] **Phase 1: Fundação** - Infraestrutura base: submodules MiroFlow/OpenClaw, tabelas skills, Neo4j, ReferenceParser
|
- [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
|
- [x] **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 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 4: OpenClaw Embutido** - Skills emergentes com detecção de padrões
|
||||||
- [ ] **Phase 5: Automation Fabric** - Automações unificadas (XOS + Central)
|
- [ ] **Phase 5: Automation Fabric** - Automações unificadas (XOS + Central)
|
||||||
- [ ] **Phase 6: Dev Center Completo** - Fábrica de agentes: Design → Assemble → Deploy
|
- [ ] **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-01: SkillEngine + API REST
|
||||||
- [x] 02-02: Editor Monaco + autocomplete + rota /skills
|
- [x] 02-02: Editor Monaco + autocomplete + rota /skills
|
||||||
- [x] 02-03: Skill Marketplace (Biblioteca)
|
- [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
|
### Phase 3: MiroFlow Embutido
|
||||||
**Goal**: Análises científicas disponíveis via agentes especializados integrados ao Superset
|
**Goal**: Análises científicas disponíveis via agentes especializados integrados ao Superset
|
||||||
|
|
@ -62,7 +62,7 @@ Plans:
|
||||||
Plans:
|
Plans:
|
||||||
- [x] 03-01-PLAN.md — Setup (ollama pull llama3.1:8b) + miroflow_service.py FastAPI porta 8006 com 3 agentes
|
- [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
|
- [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
|
### Phase 4: OpenClaw Embutido
|
||||||
**Goal**: Skills emergentes criadas automaticamente a partir de padrões detectados
|
**Goal**: Skills emergentes criadas automaticamente a partir de padrões detectados
|
||||||
|
|
@ -99,8 +99,18 @@ Plans:
|
||||||
| Phase | Plans Complete | Status | Completed |
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Fundação | 3/3 | Complete | 2026-03-25 |
|
| 1. Fundação | 3/3 | Complete | 2026-03-25 |
|
||||||
| 2. Skills Engine | 3/4 | In progress | - |
|
| 2. Skills Engine | 4/4 | Complete | 2026-03-26 |
|
||||||
| 3. MiroFlow Embutido | 2/3 | In Progress| |
|
| 3. MiroFlow Embutido | 3/3 | Complete | 2026-03-26 |
|
||||||
| 4. OpenClaw Embutido | 0/TBD | Not started | - |
|
| 4. OpenClaw Embutido | 0/TBD | Not started | - |
|
||||||
| 5. Automation Fabric | 0/TBD | Not started | - |
|
| 5. Automation Fabric | 0/TBD | Not started | - |
|
||||||
| 6. Dev Center Completo | 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)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
# State
|
# State
|
||||||
|
|
||||||
## Current Phase: 3 — MiroFlow Embutido
|
## Current Phase: 4 — OpenClaw Embutido
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
- Phase 1: submodules, tabelas, Neo4j, ReferenceParser
|
- Phase 1: submodules, tabelas, Neo4j, ReferenceParser
|
||||||
- Phase 2: SkillEngine, API REST, Monaco Editor, /skills, autocomplete, Marketplace
|
- 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
|
||||||
## Completed Plans
|
|
||||||
- Phase 3, Plan 01: miroflow_service.py + tests (60f1c5c, 76e1d34)
|
|
||||||
|
|
||||||
## In Progress
|
## 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
|
## Notes
|
||||||
- Superset em produção com RLS configurado — não alterar sem confirmação
|
- Superset em produção com RLS configurado — não alterar sem confirmação
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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<string | null>(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 (
|
||||||
|
<div className="h-full flex flex-col bg-gray-900 text-white">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-5 py-4 border-b border-white/10 flex items-center gap-2">
|
||||||
|
<Sparkles className="w-4 h-4 text-yellow-400" />
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold">Skills Emergentes</h2>
|
||||||
|
<p className="text-[11px] text-white/40">OpenClaw — detecção automática de padrões</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex border-b border-white/10 text-xs">
|
||||||
|
<button
|
||||||
|
onClick={() => setTab("suggestions")}
|
||||||
|
className={`px-4 py-2.5 flex items-center gap-1.5 transition-colors border-b-2 ${tab === "suggestions" ? "border-yellow-400 text-yellow-400" : "border-transparent text-white/50 hover:text-white"}`}
|
||||||
|
>
|
||||||
|
<Sparkles className="w-3 h-3" />
|
||||||
|
Sugestões
|
||||||
|
{suggestions.length > 0 && (
|
||||||
|
<span className="ml-1 rounded-full bg-yellow-400 text-black text-[9px] font-bold w-4 h-4 flex items-center justify-center">
|
||||||
|
{suggestions.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setTab("patterns")}
|
||||||
|
className={`px-4 py-2.5 flex items-center gap-1.5 transition-colors border-b-2 ${tab === "patterns" ? "border-yellow-400 text-yellow-400" : "border-transparent text-white/50 hover:text-white"}`}
|
||||||
|
>
|
||||||
|
<Activity className="w-3 h-3" />
|
||||||
|
Padrões
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
{/* Sugestões */}
|
||||||
|
{tab === "suggestions" && (
|
||||||
|
<div>
|
||||||
|
{sugLoading && (
|
||||||
|
<div className="flex justify-center py-10">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin text-white/30" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!sugLoading && suggestions.length === 0 && (
|
||||||
|
<div className="px-5 py-12 text-center">
|
||||||
|
<Sparkles className="w-8 h-8 text-white/10 mx-auto mb-3" />
|
||||||
|
<p className="text-sm text-white/30">Nenhuma sugestão pendente</p>
|
||||||
|
<p className="text-xs text-white/20 mt-1">O detector roda a cada hora</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="divide-y divide-white/5">
|
||||||
|
{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 (
|
||||||
|
<div key={s.id} className="px-4 py-4 space-y-2.5">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-white truncate">{s.suggestedSkillName}</p>
|
||||||
|
{s.suggestedDescription && (
|
||||||
|
<p className="text-xs text-white/50 mt-0.5 line-clamp-2">{s.suggestedDescription}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Badge className="shrink-0 bg-yellow-400/10 text-yellow-400 border-yellow-400/20 text-[10px]">
|
||||||
|
{conf}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{s.draftBody && (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(isExp ? null : s.id)}
|
||||||
|
className="flex items-center gap-1 text-[11px] text-white/40 hover:text-white/60 transition-colors"
|
||||||
|
>
|
||||||
|
{isExp ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||||
|
{isExp ? "Ocultar body" : "Ver body gerado"}
|
||||||
|
</button>
|
||||||
|
{isExp && (
|
||||||
|
<pre className="mt-2 rounded bg-black/40 p-3 text-[11px] text-white/60 overflow-x-auto whitespace-pre-wrap font-mono max-h-40 border border-white/5">
|
||||||
|
{s.draftBody}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs bg-emerald-900/40 text-emerald-400 border border-emerald-700/40 hover:bg-emerald-900/60"
|
||||||
|
onClick={() => accept.mutate(s.id)}
|
||||||
|
disabled={isAccepting || isRejecting}
|
||||||
|
>
|
||||||
|
{isAccepting ? <Loader2 className="w-3 h-3 animate-spin" /> : <CheckCircle className="w-3 h-3" />}
|
||||||
|
<span className="ml-1">Aceitar</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||||
|
onClick={() => reject.mutate(s.id)}
|
||||||
|
disabled={isAccepting || isRejecting}
|
||||||
|
>
|
||||||
|
{isRejecting ? <Loader2 className="w-3 h-3 animate-spin" /> : <XCircle className="w-3 h-3" />}
|
||||||
|
<span className="ml-1">Rejeitar</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Padrões */}
|
||||||
|
{tab === "patterns" && (
|
||||||
|
<div>
|
||||||
|
{patLoading && (
|
||||||
|
<div className="flex justify-center py-10">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin text-white/30" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!patLoading && patterns.length === 0 && (
|
||||||
|
<div className="px-5 py-12 text-center">
|
||||||
|
<BarChart2 className="w-8 h-8 text-white/10 mx-auto mb-3" />
|
||||||
|
<p className="text-sm text-white/30">Nenhum padrão detectado</p>
|
||||||
|
<p className="text-xs text-white/20 mt-1">Execute skills manualmente para gerar dados</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="divide-y divide-white/5">
|
||||||
|
{patterns.map((p) => {
|
||||||
|
const conf = Math.round(Number(p.confidence) * 100);
|
||||||
|
const last = new Date(p.lastSeenAt).toLocaleDateString("pt-BR");
|
||||||
|
return (
|
||||||
|
<div key={p.id} className="px-4 py-3 space-y-1">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<p className="text-xs font-mono text-white/70 truncate">{p.actionType}</p>
|
||||||
|
<Badge className="shrink-0 bg-white/5 text-white/50 border-white/10 text-[10px]">
|
||||||
|
{conf}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{p.description && (
|
||||||
|
<p className="text-[11px] text-white/40">{p.description}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-3 text-[10px] text-white/30">
|
||||||
|
<span className="flex items-center gap-1"><Activity className="w-3 h-3" />{p.frequency}x</span>
|
||||||
|
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{last}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -25,8 +25,9 @@ import PageBuilder from "./PageBuilder";
|
||||||
import WorkflowBuilder from "./WorkflowBuilder";
|
import WorkflowBuilder from "./WorkflowBuilder";
|
||||||
import IDE from "./IDE";
|
import IDE from "./IDE";
|
||||||
import DevAgent from "@/components/lowcode/DevAgent";
|
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 {
|
interface Dashboard {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -259,6 +260,15 @@ export default function DevelopmentModule() {
|
||||||
count: 0,
|
count: 0,
|
||||||
category: "dev"
|
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();
|
const [, navigate] = useLocation();
|
||||||
|
|
@ -705,6 +715,7 @@ export default function DevelopmentModule() {
|
||||||
{activeTool === "scripts" && renderScriptEditor()}
|
{activeTool === "scripts" && renderScriptEditor()}
|
||||||
{activeTool === "ide" && <IDE />}
|
{activeTool === "ide" && <IDE />}
|
||||||
{activeTool === "agent" && <DevAgent />}
|
{activeTool === "agent" && <DevAgent />}
|
||||||
|
{activeTool === "openclaw" && <OpenClawPanel />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={showNewDashboardDialog} onOpenChange={setShowNewDashboardDialog}>
|
<Dialog open={showNewDashboardDialog} onOpenChange={setShowNewDashboardDialog}>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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<void> {
|
||||||
|
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<Date>`MIN(${skillExecutions.startedAt})`,
|
||||||
|
lastSeen: sql<Date>`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<void> {
|
||||||
|
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<typeof setInterval> | 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -65,6 +65,8 @@ import { loadModuleRoutes } from "./modules/loader";
|
||||||
import graphRoutes from "./graph/routes";
|
import graphRoutes from "./graph/routes";
|
||||||
import { initSocketIO } from "./socket-io";
|
import { initSocketIO } from "./socket-io";
|
||||||
import { startXosScheduler } from "./xos/scheduler";
|
import { startXosScheduler } from "./xos/scheduler";
|
||||||
|
import { registerOpenClawRoutes } from "./openclaw/routes";
|
||||||
|
import { startPatternDetector } from "./openclaw/pattern-detector";
|
||||||
|
|
||||||
export async function registerRoutes(
|
export async function registerRoutes(
|
||||||
httpServer: Server,
|
httpServer: Server,
|
||||||
|
|
@ -161,6 +163,10 @@ export async function registerRoutes(
|
||||||
// XOS Scheduler: SLA breach checker + supervisor stats broadcaster
|
// XOS Scheduler: SLA breach checker + supervisor stats broadcaster
|
||||||
startXosScheduler();
|
startXosScheduler();
|
||||||
|
|
||||||
|
// OpenClaw — PatternDetector (Phase 4)
|
||||||
|
registerOpenClawRoutes(app);
|
||||||
|
startPatternDetector();
|
||||||
|
|
||||||
// Central de Protocolos (MCP, A2A, AP2, UCP)
|
// Central de Protocolos (MCP, A2A, AP2, UCP)
|
||||||
app.use("/api", protocolsRoutes);
|
app.use("/api", protocolsRoutes);
|
||||||
registerAgentCard(app); // Agent Card na raiz (/.well-known/agent.json)
|
registerAgentCard(app); // Agent Card na raiz (/.well-known/agent.json)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '../../db/index';
|
||||||
import { Skill } from './engine';
|
import { Skill } from './engine';
|
||||||
|
|
||||||
export interface SkillVersion {
|
export interface SkillVersion {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue