arcadiasuite/server/skills/routes.ts

217 lines
7.7 KiB
TypeScript

import type { Express, Request, Response } from "express";
import { db } from "../../db/index";
import {
arcadiaSkills,
skillExecutions,
insertArcadiaSkillSchema,
} from "@shared/schema";
import { eq, and, desc, ilike, or } from "drizzle-orm";
import { z } from "zod";
import { skillEngine } from "./engine";
const executeBodySchema = z.object({
inputParams: z.record(z.unknown()).optional(),
triggeredBy: z
.enum(["manual", "schedule", "automation", "agent", "webhook", "openclaw", "event"])
.optional()
.default("manual"),
automationId: z.number().int().optional(),
parentExecutionId: z.string().uuid().optional(),
});
const listQuerySchema = z.object({
namespace: z.string().optional(),
status: z.string().optional(),
search: z.string().optional(),
limit: z.coerce.number().int().min(1).max(200).optional().default(50),
offset: z.coerce.number().int().min(0).optional().default(0),
});
function tenantId(req: Request): number | undefined {
return (req as any).user?.tenantId ?? (req as any).tenantId;
}
function userId(req: Request): string | undefined {
return (req as any).user?.id ?? (req as any).userId;
}
export function registerSkillRoutes(app: Express): void {
// ── Health ────────────────────────────────────────────────────────────────
app.get("/api/skills/health", async (_req: Request, res: Response) => {
try {
const status = await skillEngine.health();
res.json(status);
} catch (err: any) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ── Listar skills ─────────────────────────────────────────────────────────
app.get("/api/skills", async (req: Request, res: Response) => {
try {
const q = listQuerySchema.parse(req.query);
const tid = tenantId(req);
const conditions: any[] = [];
if (tid) conditions.push(eq(arcadiaSkills.tenantId, tid));
if (q.namespace) conditions.push(eq(arcadiaSkills.namespace, q.namespace));
if (q.status) conditions.push(eq(arcadiaSkills.status, q.status as any));
if (q.search) {
conditions.push(
or(
ilike(arcadiaSkills.name, `%${q.search}%`),
ilike(arcadiaSkills.slug, `%${q.search}%`),
ilike(arcadiaSkills.description, `%${q.search}%`)
)
);
}
const skills = await db
.select()
.from(arcadiaSkills)
.where(conditions.length ? and(...conditions) : undefined)
.orderBy(desc(arcadiaSkills.createdAt))
.limit(q.limit)
.offset(q.offset);
res.json({ skills, total: skills.length });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
// ── Criar skill ───────────────────────────────────────────────────────────
app.post("/api/skills", async (req: Request, res: Response) => {
try {
const tid = tenantId(req);
const uid = userId(req);
const data = insertArcadiaSkillSchema.parse({
...req.body,
tenantId: tid,
createdBy: uid,
});
const [skill] = await db.insert(arcadiaSkills).values(data).returning();
res.status(201).json(skill);
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
// ── Buscar skill por ID ───────────────────────────────────────────────────
app.get("/api/skills/:id", async (req: Request, res: Response) => {
try {
const [skill] = await db
.select()
.from(arcadiaSkills)
.where(eq(arcadiaSkills.id, req.params.id))
.limit(1);
if (!skill) return res.status(404).json({ error: "Skill não encontrada" });
res.json(skill);
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
// ── Atualizar skill ───────────────────────────────────────────────────────
app.put("/api/skills/:id", async (req: Request, res: Response) => {
try {
const [existing] = await db
.select()
.from(arcadiaSkills)
.where(eq(arcadiaSkills.id, req.params.id))
.limit(1);
if (!existing) return res.status(404).json({ error: "Skill não encontrada" });
const [updated] = await db
.update(arcadiaSkills)
.set({ ...req.body, updatedAt: new Date() })
.where(eq(arcadiaSkills.id, req.params.id))
.returning();
res.json(updated);
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
// ── Deletar skill ─────────────────────────────────────────────────────────
app.delete("/api/skills/:id", async (req: Request, res: Response) => {
try {
const result = await db
.delete(arcadiaSkills)
.where(eq(arcadiaSkills.id, req.params.id));
if ((result.rowCount ?? 0) === 0) {
return res.status(404).json({ error: "Skill não encontrada" });
}
res.json({ deleted: true });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
// ── Executar skill ────────────────────────────────────────────────────────
app.post("/api/skills/:id/execute", async (req: Request, res: Response) => {
try {
const body = executeBodySchema.parse(req.body);
const result = await skillEngine.execute({
skillId: req.params.id,
inputParams: body.inputParams,
context: {
tenantId: tenantId(req),
userId: userId(req),
},
triggeredBy: body.triggeredBy,
automationId: body.automationId,
parentExecutionId: body.parentExecutionId,
});
const statusCode = result.status === "success" ? 200 : 500;
res.status(statusCode).json(result);
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
// ── Histórico de execuções ────────────────────────────────────────────────
app.get("/api/skills/:id/executions", async (req: Request, res: Response) => {
try {
const limit = Math.min(parseInt(req.query.limit as string) || 50, 200);
const executions = await skillEngine.getExecutions(req.params.id, limit);
res.json({ executions });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
// ── Execução por ID ───────────────────────────────────────────────────────
app.get("/api/skills/executions/:executionId", async (req: Request, res: Response) => {
try {
const [execution] = await db
.select()
.from(skillExecutions)
.where(eq(skillExecutions.id, req.params.executionId))
.limit(1);
if (!execution) return res.status(404).json({ error: "Execução não encontrada" });
res.json(execution);
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
}