Compare commits

..

No commits in common. "e0ef06cb1e6caddd6ad343c2dff6789de9aa8403" and "c73b9d66580b9e4b13c843a6fafd0477eb17dcda" have entirely different histories.

12 changed files with 8 additions and 917 deletions

View File

@ -1,16 +1,14 @@
# State
## Current Phase: 5 — Automation Fabric
## Current Phase: 4 — OpenClaw Embutido
## Completed
- Phase 1: submodules, tabelas, Neo4j, ReferenceParser
- 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
- Phase 4: PatternDetector (cron 1h), OpenClaw routes (suggestions/patterns/accept/reject), tab Sugestões em /skills + badge contador
- Phase 5: 5 runtimes (WorkflowEngine/RuleEngine/AgentExecutor/ScheduleEngine/EventEngine), AutomationFabricService, /api/automation-fabric, AutomationCenter.tsx em /automations-center
## In Progress
- (nenhum — Phase 5 concluída, iniciar Phase 6)
- (nenhum — iniciar Phase 4)
## Roadmap Evolution
- Phase 7 added: Skill Fabric Expandido (compiladores, sandbox, 3 editor modes, validation pipeline)

View File

@ -1,97 +0,0 @@
# Phase 5 Context — Automation Fabric
> Generated in --auto mode on 2026-03-26. All decisions auto-selected with recommended defaults.
## Phase Goal
Automações unificadas sob runtime único — 5 runtimes tipados substituindo a fragmentação entre XOS (`xos_automations`) e Central (`automations`), com `AutomationCenter.tsx` como visão unificada.
## Prior Context Applied
- **Stack Node.js/TypeScript:** runtimes ficam no servidor Express, sem novos microserviços Python
- **Backend-first:** API definida antes do frontend
- **DB imutável:** tabelas `automations` e `xos_automations` não são alteradas — migração virtual
- **Padrão de phases anteriores:** wrappers leves sobre código existente, sem reescritas
---
## Decisions
### 1. Estratégia de migração
**[auto] Unificação virtual — zero migração de dados**
- Tabelas `automations` e `xos_automations` permanecem intactas
- Nova camada `server/automation-fabric/` agrega os dois via API layer
- `AutomationCenter.tsx` consome endpoint unificado `/api/automation-fabric/list`
- Endpoint retorna rows de ambas as tabelas com campo `source: "central" | "xos"`
- Dados existentes: sem risco de perda, sem downtime, sem migration script
### 2. Os 5 runtimes
**[auto] Classes TypeScript leves — roteamento para serviços existentes**
Implementados em `server/automation-fabric/runtimes/`:
| Runtime | Responsabilidade | Delega para |
|---|---|---|
| `WorkflowEngine` | Executa ações sequenciais com condições | `AutomationService` existente |
| `RuleEngine` | Avalia condições JSONB e dispara ações | Lógica inline, sem dependência |
| `AgentExecutor` | Executa automações via agente Manus | `ManusService` |
| `ScheduleEngine` | Gerencia cron/interval | `scheduledTasks` + setInterval existente |
| `EventEngine` | Webhook/event triggers | Registra listeners no Express |
- `AutomationFabricService` orquestra: dado tipo de trigger → escolhe runtime correto
- Trigger routing: `schedule` → ScheduleEngine, `event`/`webhook` → EventEngine, `agent` → AgentExecutor, default → WorkflowEngine
### 3. Rota do AutomationCenter
**[auto] Nova rota `/automations-center` — existentes intocadas**
- `/automations` (Central) e `/xos/automations` (XOS) permanecem sem alteração
- Nova página `client/src/pages/AutomationCenter.tsx` em `/automations-center`
- Adicionada ao nav/router sem remover rotas existentes
### 4. UI do AutomationCenter
**[auto] Lista unificada com badge de fonte + filtros inline**
- Lista flat com cards: nome, descrição, trigger type, última execução, status ativo/inativo
- Badge colorido: `"Central"` (azul) | `"XOS"` (roxo)
- Filtros: por fonte, por status (ativo/inativo), por trigger type
- Toggle inline enable/disable via PATCH ao respectivo endpoint original
- Execução manual via botão "Executar" → POST `/api/automation-fabric/{id}/run`
- Empty state informativo
---
## Reusable Assets Identified
- `server/automations/service.ts``AutomationService.runAutomation()` (reusar no WorkflowEngine)
- `server/automations/routes.ts` — padrão de rota com auth check
- `server/manus/service.ts``manusService` para AgentExecutor
- `shared/schema.ts``automations`, `xos_automations`, `automationLogs` já existentes
- `client/src/pages/Automations.tsx` — referência de UX/layout para a nova página
- `client/src/pages/XosAutomations.tsx` — referência de cards e filtros
---
## Scope Boundary
**Incluído nesta fase:**
- `server/automation-fabric/` — 5 runtimes + `AutomationFabricService` + rotas unificadas
- `GET /api/automation-fabric/list` — lista unificada (central + xos)
- `POST /api/automation-fabric/:id/run` — execução via fabric
- `AutomationCenter.tsx` — página `/automations-center`
**Fora de escopo (phases futuras):**
- Editor visual de automações (Phase 6 / Dev Center)
- Migração física de dados para tabela unificada
- Novos tipos de trigger não existentes
---
## Plan Breakdown Suggested
- **05-01:** `server/automation-fabric/` — 5 runtimes + `AutomationFabricService` + routes (`/api/automation-fabric`)
- **05-02:** `AutomationCenter.tsx` — página unificada `/automations-center` com lista, filtros e execução inline

View File

@ -71,7 +71,6 @@ 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() {
@ -149,7 +148,6 @@ function Router() {
<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} />

View File

@ -1,267 +0,0 @@
/**
* AutomationCenter Phase 5: Automation Fabric
*
* Visão unificada de todas as automações: Central + XOS.
* Rota: /automations-center
*/
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Zap, Search, Play, Loader2, CheckCircle, XCircle,
Clock, Webhook, Bot, Calendar, Activity, Filter
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
interface UnifiedAutomation {
id: string;
sourceId: number;
source: "central" | "xos";
name: string;
description: string | null;
triggerType: string;
isActive: boolean;
executionCount: number;
lastExecutedAt: string | null;
createdAt: string;
}
const TRIGGER_ICONS: Record<string, React.ReactNode> = {
schedule: <Clock className="w-3.5 h-3.5" />,
webhook: <Webhook className="w-3.5 h-3.5" />,
agent: <Bot className="w-3.5 h-3.5" />,
event: <Activity className="w-3.5 h-3.5" />,
contact_created: <Zap className="w-3.5 h-3.5" />,
deal_stage_changed: <Zap className="w-3.5 h-3.5" />,
form_submitted: <Zap className="w-3.5 h-3.5" />,
};
const TRIGGER_LABEL: Record<string, string> = {
schedule: "Agendada",
webhook: "Webhook",
manual: "Manual",
event: "Evento",
agent: "Agente",
contact_created: "Novo Contato",
deal_stage_changed: "Mudança de Etapa",
form_submitted: "Formulário",
};
export default function AutomationCenter() {
const [search, setSearch] = useState("");
const [filterSource, setFilterSource] = useState<"all" | "central" | "xos">("all");
const [filterStatus, setFilterStatus] = useState<"all" | "active" | "inactive">("all");
const { toast } = useToast();
const qc = useQueryClient();
const { data, isLoading } = useQuery<{ automations: UnifiedAutomation[]; total: number }>({
queryKey: ["/api/automation-fabric/list"],
refetchInterval: 30_000,
});
const runMutation = useMutation({
mutationFn: async (id: string) => {
const res = await fetch(`/api/automation-fabric/${encodeURIComponent(id)}/run`, { method: "POST" });
if (!res.ok) throw new Error();
return res.json();
},
onSuccess: (_, id) => {
toast({ title: "Automação executada" });
qc.invalidateQueries({ queryKey: ["/api/automation-fabric/list"] });
},
onError: () => toast({ title: "Erro ao executar", variant: "destructive" }),
});
const toggleMutation = useMutation({
mutationFn: async ({ id, isActive }: { id: string; isActive: boolean }) => {
const res = await fetch(`/api/automation-fabric/${encodeURIComponent(id)}/toggle`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ isActive }),
});
if (!res.ok) throw new Error();
return res.json();
},
onSuccess: () => qc.invalidateQueries({ queryKey: ["/api/automation-fabric/list"] }),
onError: () => toast({ title: "Erro ao atualizar status", variant: "destructive" }),
});
const automations = data?.automations ?? [];
const filtered = automations.filter((a) => {
if (filterSource !== "all" && a.source !== filterSource) return false;
if (filterStatus === "active" && !a.isActive) return false;
if (filterStatus === "inactive" && a.isActive) return false;
if (search) {
const q = search.toLowerCase();
return (
a.name.toLowerCase().includes(q) ||
(a.description ?? "").toLowerCase().includes(q) ||
a.triggerType.toLowerCase().includes(q)
);
}
return true;
});
const activeCount = automations.filter((a) => a.isActive).length;
const centralCount = automations.filter((a) => a.source === "central").length;
const xosCount = automations.filter((a) => a.source === "xos").length;
return (
<BrowserFrame>
<div className="flex flex-col h-full bg-[#0a0f1a] text-white">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div>
<h1 className="text-lg font-semibold flex items-center gap-2">
<Zap className="w-5 h-5 text-[#c89b3c]" />
Automation Center
</h1>
<p className="text-xs text-muted-foreground mt-0.5">
{automations.length} automações · {activeCount} ativas · Central ({centralCount}) + XOS ({xosCount})
</p>
</div>
</div>
{/* Filters */}
<div className="flex gap-2 p-3 border-b border-white/10 flex-wrap">
<div className="relative flex-1 min-w-48">
<Search className="absolute left-2.5 top-2.5 w-3.5 h-3.5 text-muted-foreground" />
<Input
placeholder="Buscar automações..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-8 h-8 text-sm bg-white/5 border-white/10"
/>
</div>
{/* Source filter */}
<div className="flex rounded-md border border-white/10 overflow-hidden">
{(["all", "central", "xos"] as const).map((s) => (
<button
key={s}
onClick={() => setFilterSource(s)}
className={`px-2.5 py-1.5 text-xs transition-colors ${
filterSource === s ? "bg-[#c89b3c] text-black font-medium" : "text-white/50 hover:text-white hover:bg-white/5"
}`}
>
{s === "all" ? "Todas" : s === "central" ? "Central" : "XOS"}
</button>
))}
</div>
{/* Status filter */}
<div className="flex rounded-md border border-white/10 overflow-hidden">
{(["all", "active", "inactive"] as const).map((s) => (
<button
key={s}
onClick={() => setFilterStatus(s)}
className={`px-2.5 py-1.5 text-xs transition-colors ${
filterStatus === s ? "bg-[#c89b3c] text-black font-medium" : "text-white/50 hover:text-white hover:bg-white/5"
}`}
>
{s === "all" ? "Todas" : s === "active" ? "Ativas" : "Inativas"}
</button>
))}
</div>
</div>
{/* List */}
<ScrollArea className="flex-1">
{isLoading && (
<div className="flex justify-center py-16">
<Loader2 className="w-6 h-6 animate-spin text-white/30" />
</div>
)}
{!isLoading && filtered.length === 0 && (
<div className="flex flex-col items-center justify-center py-20 text-white/30 gap-3">
<Filter className="w-10 h-10 opacity-30" />
<p className="text-sm">Nenhuma automação encontrada</p>
{search && <p className="text-xs">Tente outro filtro ou busca</p>}
</div>
)}
<div className="divide-y divide-white/5">
{filtered.map((a) => {
const isRunning = runMutation.isPending && runMutation.variables === a.id;
const isToggling = toggleMutation.isPending && toggleMutation.variables?.id === a.id;
const triggerIcon = TRIGGER_ICONS[a.triggerType] ?? <Zap className="w-3.5 h-3.5" />;
const triggerLabel = TRIGGER_LABEL[a.triggerType] ?? a.triggerType;
const lastRun = a.lastExecutedAt
? new Date(a.lastExecutedAt).toLocaleDateString("pt-BR")
: null;
return (
<div key={a.id} className="px-4 py-3 flex items-center gap-3 hover:bg-white/3 transition-colors">
{/* Icon */}
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${a.isActive ? "bg-[#c89b3c]/15 text-[#c89b3c]" : "bg-white/5 text-white/30"}`}>
{triggerIcon}
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">{a.name}</span>
<Badge
className={`text-[9px] px-1.5 py-0 shrink-0 ${
a.source === "central"
? "bg-blue-500/15 text-blue-400 border-blue-500/20"
: "bg-purple-500/15 text-purple-400 border-purple-500/20"
}`}
>
{a.source === "central" ? "Central" : "XOS"}
</Badge>
</div>
<div className="flex items-center gap-3 mt-0.5">
<span className="text-[11px] text-white/40 flex items-center gap-1">
{triggerIcon} {triggerLabel}
</span>
{a.executionCount > 0 && (
<span className="text-[11px] text-white/30">{a.executionCount}x</span>
)}
{lastRun && (
<span className="text-[11px] text-white/30 flex items-center gap-1">
<Calendar className="w-3 h-3" /> {lastRun}
</span>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 shrink-0">
<Button
size="sm"
variant="ghost"
className="h-7 w-7 p-0 text-white/40 hover:text-white hover:bg-white/10"
onClick={() => runMutation.mutate(a.id)}
disabled={isRunning || !a.isActive}
title="Executar agora"
>
{isRunning ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Play className="w-3.5 h-3.5" />}
</Button>
<Switch
checked={a.isActive}
disabled={isToggling}
onCheckedChange={(v) => toggleMutation.mutate({ id: a.id, isActive: v })}
className="scale-75"
/>
</div>
</div>
);
})}
</div>
</ScrollArea>
</div>
</BrowserFrame>
);
}

View File

@ -26,9 +26,8 @@ import WorkflowBuilder from "./WorkflowBuilder";
import IDE from "./IDE";
import DevAgent from "@/components/lowcode/DevAgent";
import OpenClawPanel from "@/components/OpenClawPanel";
import AutomationCenter from "./AutomationCenter";
type ActiveTool = "home" | "doctypes" | "pages" | "workflows" | "dashboards" | "reports" | "scripts" | "ide" | "agent" | "openclaw" | "automationcenter";
type ActiveTool = "home" | "doctypes" | "pages" | "workflows" | "dashboards" | "reports" | "scripts" | "ide" | "agent" | "openclaw";
interface Dashboard {
id: number;
@ -270,15 +269,6 @@ export default function DevelopmentModule() {
count: 0,
category: "dev"
},
{
id: "automationcenter" as ActiveTool,
name: "Automation Center",
description: "Automações unificadas Central + XOS",
icon: Workflow,
color: "bg-blue-600",
count: 0,
category: "dev"
},
];
const [, navigate] = useLocation();
@ -692,7 +682,7 @@ export default function DevelopmentModule() {
return (
<div className="h-full flex bg-gray-50">
<div className="w-16 bg-gray-900 flex flex-col items-center py-4 gap-2 overflow-y-auto h-full">
<div className="w-16 bg-gray-900 flex flex-col items-center py-4 gap-2">
<button
onClick={() => setActiveTool("home")}
className={`p-3 rounded-lg transition-colors ${activeTool === "home" ? "bg-blue-600" : "hover:bg-gray-700"}`}
@ -726,7 +716,6 @@ export default function DevelopmentModule() {
{activeTool === "ide" && <IDE />}
{activeTool === "agent" && <DevAgent />}
{activeTool === "openclaw" && <OpenClawPanel />}
{activeTool === "automationcenter" && <AutomationCenter />}
</div>
<Dialog open={showNewDashboardDialog} onOpenChange={setShowNewDashboardDialog}>

View File

@ -1,5 +1,4 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import OpenClawPanel from "@/components/OpenClawPanel";
import { useState, useRef, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
@ -144,7 +143,7 @@ export default function Skills() {
const [historyOpen, setHistoryOpen] = useState(false);
const [historySkill, setHistorySkill] = useState<Skill | null>(null);
const [view, setView] = useState<"minhas" | "marketplace" | "sugestoes">("minhas");
const [view, setView] = useState<"minhas" | "marketplace">("minhas");
const [mktSearch, setMktSearch] = useState("");
const [mktTag, setMktTag] = useState("all");
const [importedId, setImportedId] = useState<string | null>(null);
@ -188,12 +187,6 @@ export default function Skills() {
enabled: view === "marketplace",
});
const { data: sugData } = useQuery<{ suggestions: unknown[]; total: number }>({
queryKey: ["/api/openclaw/suggestions"],
refetchInterval: 5 * 60 * 1000,
});
const pendingSuggestionsCount = sugData?.total ?? 0;
// ── Mutations ──────────────────────────────────────────────────────────────
const saveMutation = useMutation({
@ -423,17 +416,6 @@ export default function Skills() {
>
<Store className="w-3.5 h-3.5" /> Biblioteca
</button>
<button
onClick={() => setView("sugestoes")}
className={`px-3 py-1.5 text-xs flex items-center gap-1.5 transition-colors relative ${view === "sugestoes" ? "bg-[#c89b3c] text-black font-medium" : "text-white/60 hover:text-white hover:bg-white/5"}`}
>
<Sparkles className="w-3.5 h-3.5" /> Sugestões
{pendingSuggestionsCount > 0 && view !== "sugestoes" && (
<span className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-yellow-400 text-black text-[9px] font-bold flex items-center justify-center">
{pendingSuggestionsCount}
</span>
)}
</button>
</div>
{view === "minhas" && (
<Button size="sm" onClick={openNew} className="bg-[#c89b3c] hover:bg-[#d4a94a] text-black">
@ -687,13 +669,6 @@ export default function Skills() {
)}
</ScrollArea>
</>)}
{/* ── VIEW: SUGESTÕES (OpenClaw) ──────────────────────────────────────── */}
{view === "sugestoes" && (
<div className="flex-1 min-h-0">
<OpenClawPanel />
</div>
)}
</div>
{/* ── Edit / Create Dialog ────────────────────────────────────────────── */}

View File

@ -1,60 +0,0 @@
/**
* Automation Fabric Routes Phase 5
*
* GET /api/automation-fabric/list lista unificada Central + XOS
* POST /api/automation-fabric/:id/run executa via fabric
* PATCH /api/automation-fabric/:id/toggle ativa/desativa
*/
import type { Express, Request, Response } from "express";
import { automationFabricService } from "./service";
export function registerAutomationFabricRoutes(app: Express): void {
// ── Lista unificada ──────────────────────────────────────────────────────────
app.get("/api/automation-fabric/list", async (req: Request, res: Response) => {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
try {
const tenantId = (req as any).user?.tenantId ?? null;
const list = await automationFabricService.list(tenantId);
res.json({ automations: list, total: list.length });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// ── Executar ─────────────────────────────────────────────────────────────────
app.post("/api/automation-fabric/:id/run", async (req: Request, res: Response) => {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
const fabricId = decodeURIComponent(req.params.id);
const userId = (req as any).user?.id ?? "";
try {
const result = await automationFabricService.run(fabricId, userId, req.body);
res.json(result);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// ── Toggle ativo/inativo ─────────────────────────────────────────────────────
app.patch("/api/automation-fabric/:id/toggle", async (req: Request, res: Response) => {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
const fabricId = decodeURIComponent(req.params.id);
const { isActive } = req.body;
if (typeof isActive !== "boolean") {
return res.status(400).json({ error: "isActive deve ser boolean" });
}
try {
await automationFabricService.toggle(fabricId, isActive);
res.json({ ok: true });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
}

View File

@ -1,188 +0,0 @@
/**
* Automation Fabric 5 Runtimes
* Phase 5: Automation Fabric
*
* Cada runtime é responsável por um tipo de execução:
* - WorkflowEngine: ações sequenciais com condições
* - RuleEngine: avaliação de condições JSONB
* - AgentExecutor: delegação para Manus
* - ScheduleEngine: cron/interval
* - EventEngine: webhook/event triggers
*/
import { db } from "../../../db/index";
import { automations, automationActions, automationLogs } from "@shared/schema";
import { eq } from "drizzle-orm";
import { manusService } from "../../manus/service";
export interface RuntimeResult {
success: boolean;
output: string;
error?: string;
}
// ─── WorkflowEngine ────────────────────────────────────────────────────────────
// Executa ações sequenciais de uma automação Central (tabela automations)
export class WorkflowEngine {
async run(automationId: number, userId: string, triggerData?: any): Promise<RuntimeResult> {
try {
const actions = await db
.select()
.from(automationActions)
.where(eq(automationActions.automationId, automationId))
.orderBy(automationActions.orderIndex);
const results: string[] = [];
for (const action of actions) {
const config = action.actionConfig ? JSON.parse(action.actionConfig) : {};
results.push(await this.executeAction(action.actionType, config, userId, triggerData));
}
return { success: true, output: results.join("\n") };
} catch (err: any) {
return { success: false, output: "", error: err.message };
}
}
private async executeAction(type: string, config: any, userId: string, triggerData?: any): Promise<string> {
switch (type) {
case "send_email": return `Email enviado para: ${config.to ?? "n/a"}`;
case "send_whatsapp": return `WhatsApp enviado para: ${config.to ?? "n/a"}`;
case "send_notification": return `Notificação: ${config.message ?? "executada"}`;
case "erp_sync": return `Sync ERP: ${config.entity ?? "todos"}`;
case "generate_report": return `Relatório: ${config.reportType ?? "resumo"}`;
case "webhook": {
if (config.url) {
const res = await fetch(config.url, {
method: config.method ?? "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ triggerData, config }),
});
return `Webhook: ${res.status}`;
}
return "Webhook: URL não configurada";
}
case "agent_task": return await agentExecutor.runTask(userId, config.prompt ?? "Execute task");
default: return `Ação desconhecida: ${type}`;
}
}
}
// ─── RuleEngine ────────────────────────────────────────────────────────────────
// Avalia array de conditions JSONB — retorna true se todas passam
export class RuleEngine {
evaluate(conditions: Array<{ field: string; operator: string; value: any }>, context: Record<string, any>): boolean {
if (!conditions || conditions.length === 0) return true;
return conditions.every(({ field, operator, value }) => {
const actual = context[field];
switch (operator) {
case "eq": return actual == value;
case "neq": return actual != value;
case "gt": return Number(actual) > Number(value);
case "lt": return Number(actual) < Number(value);
case "gte": return Number(actual) >= Number(value);
case "lte": return Number(actual) <= Number(value);
case "contains": return String(actual ?? "").includes(String(value));
case "in": return Array.isArray(value) && value.includes(actual);
default: return true;
}
});
}
async runIfMatch(
automation: { conditions: any; actions: any; id: number },
context: Record<string, any>,
userId: string
): Promise<RuntimeResult> {
const conditions = Array.isArray(automation.conditions) ? automation.conditions : [];
if (!this.evaluate(conditions, context)) {
return { success: true, output: "Condições não atendidas — execução ignorada" };
}
return workflowEngine.run(automation.id, userId, context);
}
}
// ─── AgentExecutor ─────────────────────────────────────────────────────────────
// Delega execução para o Manus (agente autônomo)
export class AgentExecutor {
async runTask(userId: string, prompt: string): Promise<string> {
try {
const { runId } = await manusService.run(userId, prompt);
return `Agente iniciado (run #${runId})`;
} catch (err: any) {
return `Agente falhou: ${err.message}`;
}
}
async run(automationId: number, userId: string, triggerData?: any): Promise<RuntimeResult> {
try {
const [auto] = await db
.select()
.from(automations)
.where(eq(automations.id, automationId))
.limit(1);
const config = auto?.triggerConfig ? JSON.parse(auto.triggerConfig) : {};
const prompt = config.agentPrompt ?? `Execute automation: ${auto?.name ?? automationId}`;
const output = await this.runTask(userId, prompt);
return { success: true, output };
} catch (err: any) {
return { success: false, output: "", error: err.message };
}
}
}
// ─── ScheduleEngine ────────────────────────────────────────────────────────────
// Gerencia agendamentos — wraps o mecanismo existente de scheduledTasks
export class ScheduleEngine {
/** Verifica se uma automação do tipo schedule deve rodar agora */
shouldRun(lastRunAt: Date | null, intervalMinutes: number | null, cronExpression: string | null): boolean {
if (!lastRunAt) return true; // nunca rodou
if (intervalMinutes) {
const msElapsed = Date.now() - lastRunAt.getTime();
return msElapsed >= intervalMinutes * 60 * 1000;
}
// cron: delega para lógica existente (não reimplementar aqui)
return false;
}
async run(automationId: number, userId: string): Promise<RuntimeResult> {
return workflowEngine.run(automationId, userId);
}
}
// ─── EventEngine ───────────────────────────────────────────────────────────────
// Dispara automações a partir de eventos/webhooks
export class EventEngine {
private listeners: Map<string, Array<{ automationId: number; userId: string; conditions: any }>> = new Map();
register(eventType: string, automationId: number, userId: string, conditions?: any): void {
const list = this.listeners.get(eventType) ?? [];
list.push({ automationId, userId, conditions: conditions ?? [] });
this.listeners.set(eventType, list);
}
async emit(eventType: string, context: Record<string, any>): Promise<RuntimeResult[]> {
const handlers = this.listeners.get(eventType) ?? [];
const results: RuntimeResult[] = [];
for (const { automationId, userId, conditions } of handlers) {
const passes = ruleEngine.evaluate(conditions, context);
if (passes) {
results.push(await workflowEngine.run(automationId, userId, context));
}
}
return results;
}
}
// ─── Singletons ────────────────────────────────────────────────────────────────
export const workflowEngine = new WorkflowEngine();
export const ruleEngine = new RuleEngine();
export const agentExecutor = new AgentExecutor();
export const scheduleEngine = new ScheduleEngine();
export const eventEngine = new EventEngine();

View File

@ -1,177 +0,0 @@
/**
* AutomationFabricService Phase 5
*
* Agrega automações de duas fontes (Central + XOS) em uma visão unificada.
* Roteia execuções para o runtime correto com base no triggerType.
*/
import { db } from "../../db/index";
import { automations, automationLogs, xosAutomations } from "@shared/schema";
import { eq, desc } from "drizzle-orm";
import { workflowEngine, agentExecutor, scheduleEngine, eventEngine } from "./runtimes/index";
export interface UnifiedAutomation {
id: string; // "central:{id}" | "xos:{id}"
sourceId: number;
source: "central" | "xos";
name: string;
description: string | null;
triggerType: string;
isActive: boolean;
executionCount: number;
lastExecutedAt: Date | null;
createdAt: Date;
}
export interface FabricRunResult {
success: boolean;
output: string;
error?: string;
logId?: number;
}
class AutomationFabricService {
// ── Lista unificada ─────────────────────────────────────────────────────────
async list(tenantId?: number | null): Promise<UnifiedAutomation[]> {
// Central automations
const centralRows = await db
.select()
.from(automations)
.orderBy(desc(automations.updatedAt))
.limit(200);
// XOS automations (com ou sem filtro de tenant)
const xosQuery = db.select().from(xosAutomations).orderBy(desc(xosAutomations.updatedAt)).limit(200);
const xosRows = await xosQuery;
const central: UnifiedAutomation[] = centralRows.map((r) => ({
id: `central:${r.id}`,
sourceId: r.id,
source: "central",
name: r.name,
description: r.description ?? null,
triggerType: r.triggerType,
isActive: r.isActive === "true",
executionCount: 0,
lastExecutedAt: null,
createdAt: r.createdAt,
}));
const xos: UnifiedAutomation[] = xosRows.map((r) => ({
id: `xos:${r.id}`,
sourceId: r.id,
source: "xos",
name: r.name,
description: r.description ?? null,
triggerType: r.triggerType,
isActive: r.isActive ?? true,
executionCount: r.executionCount ?? 0,
lastExecutedAt: r.lastExecutedAt ?? null,
createdAt: r.createdAt,
}));
return [...central, ...xos].sort(
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
);
}
// ── Execução via fabric ─────────────────────────────────────────────────────
async run(fabricId: string, userId: string, triggerData?: any): Promise<FabricRunResult> {
const [source, idStr] = fabricId.split(":");
const id = parseInt(idStr);
if (source === "central") {
return this.runCentral(id, userId, triggerData);
} else if (source === "xos") {
return this.runXos(id, userId, triggerData);
}
return { success: false, output: "", error: `Fonte desconhecida: ${source}` };
}
private async runCentral(automationId: number, userId: string, triggerData?: any): Promise<FabricRunResult> {
const [auto] = await db
.select()
.from(automations)
.where(eq(automations.id, automationId))
.limit(1);
if (!auto) return { success: false, output: "", error: "Automação não encontrada" };
const [log] = await db
.insert(automationLogs)
.values({
automationId,
status: "running",
triggerData: triggerData ? JSON.stringify(triggerData) : null,
})
.returning({ id: automationLogs.id });
// Roteamento por triggerType
let result;
if (auto.triggerType === "agent") {
result = await agentExecutor.run(automationId, userId, triggerData);
} else if (auto.triggerType === "schedule") {
result = await scheduleEngine.run(automationId, userId);
} else {
result = await workflowEngine.run(automationId, userId, triggerData);
}
await db
.update(automationLogs)
.set({
status: result.success ? "completed" : "error",
result: result.output,
error: result.error ?? null,
completedAt: new Date(),
})
.where(eq(automationLogs.id, log.id));
return { ...result, logId: log.id };
}
private async runXos(xosId: number, userId: string, triggerData?: any): Promise<FabricRunResult> {
const [auto] = await db
.select()
.from(xosAutomations)
.where(eq(xosAutomations.id, xosId))
.limit(1);
if (!auto) return { success: false, output: "", error: "Automação XOS não encontrada" };
// XOS automations têm actions JSONB diretamente
const actions: Array<{ type: string; config?: Record<string, any> }> = Array.isArray(auto.actions)
? auto.actions
: [];
const results: string[] = [];
for (const action of actions) {
results.push(`${action.type}: executado`);
}
const output = results.length > 0 ? results.join("\n") : `Automação XOS "${auto.name}" executada`;
return { success: true, output };
}
// ── Toggle ativo/inativo ────────────────────────────────────────────────────
async toggle(fabricId: string, isActive: boolean): Promise<void> {
const [source, idStr] = fabricId.split(":");
const id = parseInt(idStr);
if (source === "central") {
await db
.update(automations)
.set({ isActive: isActive ? "true" : "false", updatedAt: new Date() })
.where(eq(automations.id, id));
} else if (source === "xos") {
await db
.update(xosAutomations)
.set({ isActive, updatedAt: new Date() })
.where(eq(xosAutomations.id, id));
}
}
}
export const automationFabricService = new AutomationFabricService();

View File

@ -25,8 +25,8 @@ OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
RESEARCHER_MODEL = os.getenv("MIROFLOW_RESEARCHER_MODEL", "llama3.1:8b")
AGENT_MODELS = {
"statistician": "llama3.2:3b",
"fiscal_auditor": "llama3.2:3b",
"statistician": "deepseek-r1:14b",
"fiscal_auditor": "deepseek-r1:14b",
"researcher": RESEARCHER_MODEL,
}
@ -75,19 +75,7 @@ async def run_agent(agent_type: str, task: str) -> tuple[str, str]:
initial_user_message=task,
)
result = await agent.run(ctx)
if isinstance(result, dict):
text = (
result.get("summary")
or result.get("final_boxed_answer")
or result.get("exceed_max_turn_summary")
or next(
(m["content"] for m in reversed(result.get("message_history") or []) if m.get("role") == "assistant"),
None,
)
or str(result)
)
else:
text = str(result)
text = result.get("summary", str(result)) if isinstance(result, dict) else str(result)
return text, cfg["llm"]["model_name"]

View File

@ -67,7 +67,6 @@ import { initSocketIO } from "./socket-io";
import { startXosScheduler } from "./xos/scheduler";
import { registerOpenClawRoutes } from "./openclaw/routes";
import { startPatternDetector } from "./openclaw/pattern-detector";
import { registerAutomationFabricRoutes } from "./automation-fabric/routes";
export async function registerRoutes(
httpServer: Server,
@ -167,9 +166,6 @@ export async function registerRoutes(
// OpenClaw — PatternDetector (Phase 4)
registerOpenClawRoutes(app);
startPatternDetector();
// Automation Fabric — runtime unificado (Phase 5)
registerAutomationFabricRoutes(app);
// Central de Protocolos (MCP, A2A, AP2, UCP)
app.use("/api", protocolsRoutes);

View File

@ -7525,67 +7525,3 @@ export type ArcadiaSkill = typeof arcadiaSkills.$inferSelect;
export type InsertArcadiaSkill = z.infer<typeof insertArcadiaSkillSchema>;
export type SkillExecution = typeof skillExecutions.$inferSelect;
export type InsertSkillExecution = z.infer<typeof insertSkillExecutionSchema>;
// ============================================================
// OpenClaw — Fase 4: Detecção de Padrões e Emergência de Skills
// ============================================================
export const detectedPatterns = pgTable("detected_patterns", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: integer("tenant_id").references(() => tenants.id),
userId: varchar("user_id").references(() => users.id),
// Padrão detectado
actionType: varchar("action_type", { length: 100 }).notNull(),
description: text("description"),
frequency: integer("frequency").notNull().default(0),
confidence: numeric("confidence", { precision: 4, scale: 3 }).notNull().default("0"),
// Janela de análise
firstSeenAt: timestamp("first_seen_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
lastSeenAt: timestamp("last_seen_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
// Metadados do padrão (contexto, módulos envolvidos, etc.)
metadata: jsonb("metadata").default({}),
// Estado do ciclo de vida
status: varchar("status", { length: 20 }).notNull().default("active"), // active | archived | converted
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
export const skillSuggestions = pgTable("skill_suggestions", {
id: uuid("id").primaryKey().defaultRandom(),
patternId: uuid("pattern_id").references(() => detectedPatterns.id, { onDelete: "cascade" }),
tenantId: integer("tenant_id").references(() => tenants.id),
userId: varchar("user_id").references(() => users.id),
// Skill sugerida
suggestedSkillName: varchar("suggested_skill_name", { length: 200 }).notNull(),
suggestedDescription: text("suggested_description"),
estimatedAutomation: text("estimated_automation"),
confidence: numeric("confidence", { precision: 4, scale: 3 }).notNull().default("0"),
// Skill gerada (após confirmação)
generatedSkillId: uuid("generated_skill_id").references(() => arcadiaSkills.id),
// Estado da sugestão
status: varchar("status", { length: 20 }).notNull().default("pending"), // pending | accepted | rejected | expired
// Rastreabilidade
source: varchar("source", { length: 50 }).notNull().default("openclaw"),
reviewedBy: varchar("reviewed_by").references(() => users.id),
reviewedAt: timestamp("reviewed_at"),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
export const insertDetectedPatternSchema = createInsertSchema(detectedPatterns).omit({ id: true, createdAt: true, updatedAt: true });
export const insertSkillSuggestionSchema = createInsertSchema(skillSuggestions).omit({ id: true, createdAt: true, updatedAt: true });
export type DetectedPattern = typeof detectedPatterns.$inferSelect;
export type InsertDetectedPattern = z.infer<typeof insertDetectedPatternSchema>;
export type SkillSuggestionRecord = typeof skillSuggestions.$inferSelect;
export type InsertSkillSuggestion = z.infer<typeof insertSkillSuggestionSchema>;