mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 06:56:44 +00:00
Adicionou diagnóstico Hermes
X-Lovable-Edit-ID: edt-e94377c4-c536-4313-882b-1a2d53063f69 Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
commit
9e6facffa1
3 changed files with 247 additions and 1 deletions
|
|
@ -575,8 +575,11 @@ function AgentDetailPage() {
|
|||
</Link>
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<RuntimeInspectSection agentInstanceId={id} />
|
||||
</div>
|
||||
|
||||
|
||||
{/* ===== Coluna direita — Status ===== */}
|
||||
<aside className="space-y-4">
|
||||
<StatusCard agent={agent} />
|
||||
|
|
@ -745,3 +748,96 @@ function HistoryCard({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface InspectResult {
|
||||
path: string;
|
||||
status: number;
|
||||
ok: boolean;
|
||||
body: unknown;
|
||||
}
|
||||
|
||||
interface InspectResponse {
|
||||
agent_instance_id: string;
|
||||
public_url: string;
|
||||
public_domain: string;
|
||||
service_id: string;
|
||||
results: InspectResult[];
|
||||
}
|
||||
|
||||
function RuntimeInspectSection({ agentInstanceId }: { agentInstanceId: string }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<InspectResponse | null>(null);
|
||||
const [pathsInput, setPathsInput] = useState(
|
||||
"/api/health,/api/cronjobs,/api/integrations,/api/plugins",
|
||||
);
|
||||
|
||||
async function handleInspect() {
|
||||
setLoading(true);
|
||||
const paths = pathsInput
|
||||
.split(",")
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.startsWith("/"));
|
||||
const { data, error } = await invokeFunction<InspectResponse>(
|
||||
"admin-runtime-inspect",
|
||||
{ agent_instance_id: agentInstanceId, paths },
|
||||
);
|
||||
setLoading(false);
|
||||
if (error || !data) {
|
||||
toast.error(error?.message ?? "Falha ao inspecionar runtime");
|
||||
return;
|
||||
}
|
||||
setResult(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-border bg-card p-6 shadow-soft space-y-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="font-semibold text-lg">Diagnóstico do runtime (Hermes)</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Faz GET autenticado nos endpoints internos do container Hermes do agente.
|
||||
Útil para verificar se o plugin de cron está ativo, jobs registrados na memória do runtime, integrações conhecidas etc.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inspect-paths" className="text-xs">
|
||||
Paths (separados por vírgula)
|
||||
</Label>
|
||||
<Input
|
||||
id="inspect-paths"
|
||||
value={pathsInput}
|
||||
onChange={(e) => setPathsInput(e.target.value)}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleInspect} disabled={loading} className="w-full">
|
||||
{loading ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : null}
|
||||
Inspecionar runtime
|
||||
</Button>
|
||||
{result && (
|
||||
<div className="space-y-3 pt-2">
|
||||
<p className="text-xs text-muted-foreground font-mono break-all">
|
||||
{result.public_url}
|
||||
</p>
|
||||
{result.results.map((r) => (
|
||||
<div
|
||||
key={r.path}
|
||||
className="rounded-md border border-border bg-muted/30 p-3 space-y-1"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<code className="text-xs font-mono">{r.path}</code>
|
||||
<Badge variant={r.ok ? "success" : "destructive"}>
|
||||
HTTP {r.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<pre className="text-xs whitespace-pre-wrap break-all max-h-64 overflow-auto">
|
||||
{typeof r.body === "string"
|
||||
? r.body
|
||||
: JSON.stringify(r.body, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -397,7 +397,7 @@ async function loadAgentServiceTarget(
|
|||
};
|
||||
}
|
||||
|
||||
async function resolveRuntimeTarget(opts: {
|
||||
export async function resolveRuntimeTarget(opts: {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
supabase: any;
|
||||
agentInstanceId: string;
|
||||
|
|
|
|||
150
supabase/functions/admin-runtime-inspect/index.ts
Normal file
150
supabase/functions/admin-runtime-inspect/index.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// admin-runtime-inspect
|
||||
// Endpoint admin para inspecionar o estado bruto do runtime Hermes de um agente.
|
||||
// Faz GET autenticado (Bearer HERMES_API_SERVER_KEY) em uma lista de paths conhecidos
|
||||
// e devolve as respostas para diagnóstico (cronjobs, plugins, integrations, health).
|
||||
//
|
||||
// Uso (admin-only):
|
||||
// POST { agent_instance_id: string, paths?: string[] }
|
||||
//
|
||||
// Default paths: /api/health, /api/cronjobs, /api/integrations, /api/plugins
|
||||
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from "../_shared/cors.ts";
|
||||
import { resolveRuntimeTarget } from "../_shared/runtime-sync.ts";
|
||||
|
||||
const DEFAULT_PATHS = [
|
||||
"/api/health",
|
||||
"/api/cronjobs",
|
||||
"/api/integrations",
|
||||
"/api/plugins",
|
||||
];
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchRuntimePath(opts: {
|
||||
publicUrl: string;
|
||||
apiKey: string;
|
||||
path: string;
|
||||
}): Promise<{ path: string; status: number; ok: boolean; body: unknown }> {
|
||||
const url = `${opts.publicUrl.replace(/\/$/, "")}${opts.path}`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${opts.apiKey}` },
|
||||
});
|
||||
const text = await res.text();
|
||||
let body: unknown = text;
|
||||
try {
|
||||
body = JSON.parse(text);
|
||||
} catch {
|
||||
// mantém texto cru
|
||||
}
|
||||
return { path: opts.path, status: res.status, ok: res.ok, body };
|
||||
} catch (err) {
|
||||
return {
|
||||
path: opts.path,
|
||||
status: 0,
|
||||
ok: false,
|
||||
body: { error: err instanceof Error ? err.message : String(err) },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
||||
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!;
|
||||
const railwayToken = Deno.env.get("RAILWAY_API_TOKEN") ?? "";
|
||||
const hermesKey = Deno.env.get("HERMES_API_SERVER_KEY") ?? "";
|
||||
|
||||
if (!railwayToken) return jsonResponse({ error: "RAILWAY_API_TOKEN not configured" }, 500);
|
||||
if (!hermesKey) return jsonResponse({ error: "HERMES_API_SERVER_KEY not configured" }, 500);
|
||||
|
||||
// 1) Autenticação: JWT do usuário e checa role admin
|
||||
const authHeader = req.headers.get("Authorization") ?? "";
|
||||
const userClient = createClient(supabaseUrl, anonKey, {
|
||||
global: { headers: { Authorization: authHeader } },
|
||||
});
|
||||
const { data: userData, error: userErr } = await userClient.auth.getUser();
|
||||
if (userErr || !userData.user) {
|
||||
return jsonResponse({ error: "Não autenticado" }, 401);
|
||||
}
|
||||
|
||||
const admin = createClient(supabaseUrl, serviceKey);
|
||||
const { data: roleRow } = await admin
|
||||
.from("user_roles")
|
||||
.select("role")
|
||||
.eq("user_id", userData.user.id)
|
||||
.eq("role", "admin")
|
||||
.maybeSingle();
|
||||
|
||||
if (!roleRow) {
|
||||
return jsonResponse({ error: "Admin requerido" }, 403);
|
||||
}
|
||||
|
||||
// 2) Body
|
||||
const body = await req.json().catch(() => ({})) as {
|
||||
agent_instance_id?: string;
|
||||
paths?: string[];
|
||||
};
|
||||
if (!body.agent_instance_id) {
|
||||
return jsonResponse({ error: "agent_instance_id é obrigatório" }, 400);
|
||||
}
|
||||
const paths = Array.isArray(body.paths) && body.paths.length > 0
|
||||
? body.paths.filter((p) => typeof p === "string" && p.startsWith("/"))
|
||||
: DEFAULT_PATHS;
|
||||
|
||||
// 3) Resolve target Railway
|
||||
let target;
|
||||
try {
|
||||
target = await resolveRuntimeTarget({
|
||||
supabase: admin,
|
||||
agentInstanceId: body.agent_instance_id,
|
||||
railwayToken,
|
||||
});
|
||||
} catch (err) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "Falha ao resolver runtime target",
|
||||
detail: err instanceof Error ? err.message : String(err),
|
||||
},
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
// 4) Faz GET em cada path em paralelo
|
||||
const results = await Promise.all(
|
||||
paths.map((p) =>
|
||||
fetchRuntimePath({
|
||||
publicUrl: target.publicUrl,
|
||||
apiKey: hermesKey,
|
||||
path: p,
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
return jsonResponse({
|
||||
agent_instance_id: body.agent_instance_id,
|
||||
public_url: target.publicUrl,
|
||||
public_domain: target.publicDomain,
|
||||
service_id: target.serviceId,
|
||||
results,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("admin-runtime-inspect fatal", err instanceof Error ? err.message : "unknown");
|
||||
return jsonResponse(
|
||||
{ error: err instanceof Error ? err.message : "Erro inesperado" },
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue