mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 15:56:50 +00:00
fix: wire mika runtime cron and skill actions
This commit is contained in:
parent
6c0ccff305
commit
e9f22e6ceb
16 changed files with 1562 additions and 1310 deletions
|
|
@ -23,12 +23,30 @@ import cronstrue from "https://esm.sh/cronstrue@2.50.0/i18n";
|
|||
import { corsHeaders } from "../_shared/cors.ts";
|
||||
import { syncAgentRuntimeSnapshot } from "../_shared/runtime-sync.ts";
|
||||
|
||||
type GenericTable = {
|
||||
Row: Record<string, unknown>;
|
||||
Insert: Record<string, unknown>;
|
||||
Update: Record<string, unknown>;
|
||||
Relationships: [];
|
||||
};
|
||||
|
||||
type GenericDatabase = {
|
||||
public: {
|
||||
Tables: Record<string, GenericTable>;
|
||||
Views: Record<string, GenericTable>;
|
||||
Functions: Record<string, { Args: Record<string, unknown>; Returns: unknown }>;
|
||||
};
|
||||
};
|
||||
|
||||
type SupabaseAdminClient = ReturnType<typeof createClient<GenericDatabase>>;
|
||||
|
||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
|
||||
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const INTERNAL_FUNCTION_SECRET = Deno.env.get("INTERNAL_FUNCTION_SECRET") ?? "";
|
||||
const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY") ?? "";
|
||||
const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN") ?? "";
|
||||
const HERMES_API_SERVER_KEY = Deno.env.get("HERMES_API_SERVER_KEY") ?? "";
|
||||
const CONTRACT_VERSION = "2026-05-28";
|
||||
|
||||
const MODEL = "google/gemini-2.5-flash";
|
||||
|
||||
|
|
@ -66,6 +84,13 @@ function constantTimeEq(a: string, b: string): boolean {
|
|||
return diff === 0;
|
||||
}
|
||||
|
||||
function isAuthorized(req: Request): boolean {
|
||||
const received = req.headers.get("x-internal-secret") ?? "";
|
||||
return (
|
||||
!!INTERNAL_FUNCTION_SECRET && !!received && constantTimeEq(INTERNAL_FUNCTION_SECRET, received)
|
||||
);
|
||||
}
|
||||
|
||||
function buildSystemPrompt(tz: string): string {
|
||||
return `Você é um parser de descrições de cronjobs em português para o assistente Mika. Receba uma descrição em linguagem natural e retorne APENAS JSON válido, sem markdown, sem explicações.
|
||||
|
||||
|
|
@ -90,12 +115,16 @@ interface ParsedJob {
|
|||
function tryParseJson(text: string): ParsedJob | null {
|
||||
try {
|
||||
return JSON.parse(text) as ParsedJob;
|
||||
} catch (_) { /* segue */ }
|
||||
} catch (_) {
|
||||
/* segue */
|
||||
}
|
||||
const match = text.match(/\{[\s\S]*\}/);
|
||||
if (match) {
|
||||
try {
|
||||
return JSON.parse(match[0]) as ParsedJob;
|
||||
} catch (_) { /* falhou */ }
|
||||
} catch (_) {
|
||||
/* falhou */
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -123,16 +152,56 @@ async function parseWithAI(input: string, tz: string): Promise<ParsedJob | null>
|
|||
return tryParseJson(content);
|
||||
}
|
||||
|
||||
async function markJobRuntimeSyncError(
|
||||
admin: SupabaseAdminClient,
|
||||
jobId: string,
|
||||
syncError: string,
|
||||
): Promise<void> {
|
||||
const message = syncError.slice(0, 2000);
|
||||
const { error } = await admin
|
||||
.from("scheduled_jobs")
|
||||
.update({
|
||||
status: "error",
|
||||
auto_paused_reason: "Falha ao sincronizar esta automação com o runtime do agente.",
|
||||
runtime_state: "error",
|
||||
runtime_last_status: "error",
|
||||
runtime_last_error: message,
|
||||
})
|
||||
.eq("id", jobId);
|
||||
|
||||
if (error) {
|
||||
console.error("failed to mark scheduled_job runtime sync error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
|
||||
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
||||
|
||||
// 1) Auth: apenas X-Internal-Secret (chamada server-to-server do runtime)
|
||||
const received = req.headers.get("x-internal-secret") ?? "";
|
||||
if (!INTERNAL_FUNCTION_SECRET || !received || !constantTimeEq(INTERNAL_FUNCTION_SECRET, received)) {
|
||||
if (!isAuthorized(req)) {
|
||||
return json({ error: "unauthorized" }, 401);
|
||||
}
|
||||
|
||||
if (req.method === "GET" || req.method === "HEAD") {
|
||||
return json({
|
||||
success: true,
|
||||
endpoint: "create-cronjob-from-agent",
|
||||
contract_version: CONTRACT_VERSION,
|
||||
expected_header: "X-Internal-Secret",
|
||||
required_body_fields: ["agent_instance_id", "natural_language_input"],
|
||||
optional_body_fields: [
|
||||
"cron_expression",
|
||||
"action_prompt",
|
||||
"required_mcp_slugs",
|
||||
"name",
|
||||
"description",
|
||||
"timezone",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
||||
|
||||
// 2) Body
|
||||
let body: RequestBody;
|
||||
try {
|
||||
|
|
@ -144,7 +213,8 @@ Deno.serve(async (req) => {
|
|||
if (!body.agent_instance_id) return json({ error: "agent_instance_id required" }, 400);
|
||||
const input = (body.natural_language_input ?? "").trim();
|
||||
if (input.length < 5) return json({ error: "natural_language_input too short" }, 400);
|
||||
if (input.length > 1000) return json({ error: "natural_language_input too long (max 1000)" }, 400);
|
||||
if (input.length > 1000)
|
||||
return json({ error: "natural_language_input too long (max 1000)" }, 400);
|
||||
|
||||
const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
|
||||
auth: { persistSession: false, autoRefreshToken: false },
|
||||
|
|
@ -172,7 +242,9 @@ Deno.serve(async (req) => {
|
|||
let cron = (body.cron_expression ?? "").trim();
|
||||
let actionPrompt = (body.action_prompt ?? "").trim();
|
||||
let reqSlugs = Array.isArray(body.required_mcp_slugs)
|
||||
? body.required_mcp_slugs.filter((s): s is string => typeof s === "string" && VALID_MCP_SLUGS.has(s))
|
||||
? body.required_mcp_slugs.filter(
|
||||
(s): s is string => typeof s === "string" && VALID_MCP_SLUGS.has(s),
|
||||
)
|
||||
: [];
|
||||
|
||||
if (!cron || !actionPrompt) {
|
||||
|
|
@ -208,7 +280,9 @@ Deno.serve(async (req) => {
|
|||
let humanReadable = cron;
|
||||
try {
|
||||
humanReadable = cronstrue.toString(cron, { locale: "pt_BR" });
|
||||
} catch (_) { /* fallback */ }
|
||||
} catch (_) {
|
||||
/* fallback */
|
||||
}
|
||||
|
||||
// 7) Nome: usa o fornecido ou deriva do input
|
||||
const name = (body.name ?? "").trim() || input.slice(0, 80);
|
||||
|
|
@ -245,7 +319,8 @@ Deno.serve(async (req) => {
|
|||
return json({ error: "failed to create cronjob", detail: insertErr.message }, 500);
|
||||
}
|
||||
|
||||
// 9) Push para o runtime (best-effort; não derruba a criação se falhar)
|
||||
// 9) Push para o runtime. Se falhar, o job fica registrado como erro,
|
||||
// mas não fica ativo na UI como se estivesse realmente agendado.
|
||||
let syncOk = false;
|
||||
let syncError: string | null = null;
|
||||
try {
|
||||
|
|
@ -259,7 +334,23 @@ Deno.serve(async (req) => {
|
|||
syncOk = true;
|
||||
} catch (e) {
|
||||
syncError = e instanceof Error ? e.message : String(e);
|
||||
console.error("runtime sync failed (job created anyway):", syncError);
|
||||
console.error("runtime sync failed; marking job as error:", syncError);
|
||||
await markJobRuntimeSyncError(admin, inserted.id, syncError);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
job_id: inserted.id,
|
||||
name: inserted.name,
|
||||
cron_expression: inserted.cron_expression,
|
||||
human_readable: inserted.human_readable,
|
||||
next_run_at: inserted.next_run_at,
|
||||
required_mcp_slugs: inserted.required_mcp_slugs,
|
||||
status: "error",
|
||||
runtime_sync_ok: false,
|
||||
runtime_sync_error: syncError,
|
||||
},
|
||||
502,
|
||||
);
|
||||
}
|
||||
|
||||
return json({
|
||||
|
|
@ -270,6 +361,7 @@ Deno.serve(async (req) => {
|
|||
human_readable: inserted.human_readable,
|
||||
next_run_at: inserted.next_run_at,
|
||||
required_mcp_slugs: inserted.required_mcp_slugs,
|
||||
status: inserted.status,
|
||||
runtime_sync_ok: syncOk,
|
||||
runtime_sync_error: syncError,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue