Automatisierungsvorschlaege: Agenten schlagen n8n-Workflows vor, der Mensch importiert von Hand (#726)
- Core/Automation/WorkflowReview: free check of an n8n export - every node with its effect and target, code/command/SSH nodes only with a stated reason, embedded secrets refused (by name or by look), a short summary of what the workflow does. - AutomationProposals: measured saving from the replaced tasks' receipts (28 days), at most 3 open per agent, the same purpose not twice, a rejected proposal must be answered; the human gets a task and decides in the approvals view. Accepting records it - importing stays a human's step. - Candidates: recurring tasks whose runs repeat the same calls (>= 75 % of >= 8 runs in 14 days) are put to their agent once a month. - Escalation tool: automation_find, automation_propose. - Approvals view: proposals with nodes, targets, warnings, saving, JSON download, accept/reject. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
64e09b82d5
commit
067ff62317
@@ -698,6 +698,20 @@
|
||||
"staging.rejected": "Aufruf wurde abgelehnt.",
|
||||
"staging.rejectFailed": "Ablehnen fehlgeschlagen: {error}",
|
||||
"staging.withLead": "Die Leitung {lead} darf dies bis {until} entscheiden, im Rahmen dessen, was ihre Einheit erlaubt. Du kannst vorher entscheiden; danach liegt es allein bei dir.",
|
||||
"automation.title": "Automatisierungsvorschläge",
|
||||
"automation.hint": "Agenten schlagen n8n-Workflows für Routine vor, die sie immer wieder gleich erledigen. Lies, was der Workflow tut — jeden Knoten und jede Adresse, die er erreicht. Nimmst du an, importierst du das JSON von Hand in n8n, testest es und gibst es als MCP-Werkzeug frei; einen Knopf, der das für dich tut, gibt es mit Absicht nicht. Eine Ablehnung behält ihren Grund.",
|
||||
"automation.none": "Kein Vorschlag wartet auf dich.",
|
||||
"automation.trigger": "Auslöser",
|
||||
"automation.savings": "Gemessen über {days} Tage: {runs} Lauf/Läufe, {cost} USD — etwa {runsMonth} Läufe und {month} USD im Monat.",
|
||||
"automation.does": "Was er tut",
|
||||
"automation.replaces": "Ersetzt Aufgaben",
|
||||
"automation.services": "Braucht",
|
||||
"automation.risks": "Risiken",
|
||||
"automation.testGuide": "Testanleitung",
|
||||
"automation.download": "JSON herunterladen",
|
||||
"automation.reason": "Grund (Pflicht beim Ablehnen)",
|
||||
"automation.accept": "Annehmen",
|
||||
"automation.reject": "Ablehnen",
|
||||
"staging.voteRunning": "Abstimmung #{id} läuft bis {until}: {yes} ja, {no} nein, {missing} ausstehend. Du kannst vorher entscheiden.",
|
||||
"staging.voteDone": "Abstimmung #{id}: {outcome} — {yes} ja, {no} nein, {missing} fehlen.",
|
||||
"staging.voteRule": "Angenommen ab {approve} Ja, abgelehnt bei höchstens {reject}, mindestens {quorum} Stimmen",
|
||||
|
||||
@@ -698,6 +698,20 @@
|
||||
"staging.rejected": "Call was rejected.",
|
||||
"staging.rejectFailed": "Rejection failed: {error}",
|
||||
"staging.withLead": "Its lead {lead} may decide this until {until}, within the limits its unit grants. You can decide it first; after that it is yours alone.",
|
||||
"automation.title": "Automation proposals",
|
||||
"automation.hint": "Agents propose n8n workflows for routine they do the same way again and again. Read what the workflow does — every node and every address it reaches. If you accept, import the JSON into n8n by hand, test it, and release it as an MCP tool; there is deliberately no button that does it for you. A rejection keeps its reason.",
|
||||
"automation.none": "No proposal waits for you.",
|
||||
"automation.trigger": "trigger",
|
||||
"automation.savings": "Measured over {days} days: {runs} run(s), {cost} USD — about {runsMonth} runs and {month} USD a month.",
|
||||
"automation.does": "What it does",
|
||||
"automation.replaces": "Replaces tasks",
|
||||
"automation.services": "Needs",
|
||||
"automation.risks": "Risks",
|
||||
"automation.testGuide": "Test guide",
|
||||
"automation.download": "Download JSON",
|
||||
"automation.reason": "Reason (required to reject)",
|
||||
"automation.accept": "Accept",
|
||||
"automation.reject": "Reject",
|
||||
"staging.voteRunning": "Vote #{id} runs until {until}: {yes} yes, {no} no, {missing} outstanding. You can decide it first.",
|
||||
"staging.voteDone": "Vote #{id}: {outcome} — {yes} yes, {no} no, {missing} missing.",
|
||||
"staging.voteRule": "Approved from {approve} yes, rejected at {reject} or fewer, at least {quorum} votes",
|
||||
|
||||
@@ -77,7 +77,8 @@ export async function rendereStaging() {
|
||||
const listeBereich = erstelleElement("div", { id: "staging-liste-bereich" });
|
||||
const dauerBereich = erstelleElement("div", { id: "staging-dauer-bereich" });
|
||||
const leitungBereich = erstelleElement("div", { id: "staging-leitung-bereich" });
|
||||
layout.append(kopfZeile, listeBereich, dauerBereich, leitungBereich);
|
||||
// #726: workflows agents propose — decided here, imported by hand.
|
||||
layout.append(kopfZeile, listeBereich, erstelleAutomationsKarte(), dauerBereich, leitungBereich);
|
||||
container.append(layout);
|
||||
|
||||
await ladeFreigaben();
|
||||
@@ -420,3 +421,76 @@ function oeffneAblehnenDialog(id) {
|
||||
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
/**
|
||||
* #726: Workflows agents propose. The human reads what the workflow does (the review lists every
|
||||
* node and every address outside), decides, and on acceptance imports the JSON into n8n by hand —
|
||||
* deliberately no "apply" button: a workflow runs outside every check here.
|
||||
*/
|
||||
export function erstelleAutomationsKarte() {
|
||||
const box = erstelleElement("div", { klasse: "automationen" });
|
||||
|
||||
async function lade() {
|
||||
let liste = [];
|
||||
try { liste = (await api("/staging/automations?status=proposed")) || []; } catch { liste = []; }
|
||||
if (!liste.length) {
|
||||
box.replaceChildren(erstelleElement("p", { klasse: "gedaempft text-klein", text: t("automation.none") }));
|
||||
return;
|
||||
}
|
||||
box.replaceChildren(...liste.map(vorschlag));
|
||||
}
|
||||
|
||||
function vorschlag(p) {
|
||||
const grund = erstelleElement("input", { type: "text", placeholder: t("automation.reason") });
|
||||
const entscheiden = async (annehmen) => {
|
||||
try {
|
||||
const r = await api(`/staging/automations/${p.id}/decide`, { method: "POST", body: { accept: annehmen, reason: grund.value } });
|
||||
zeigeMeldung(r?.message ?? t("common.saved"), "erfolg", 8000);
|
||||
await lade();
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr", 8000);
|
||||
}
|
||||
};
|
||||
const herunterladen = async () => {
|
||||
try {
|
||||
const json = await api(`/staging/automations/${p.id}/workflow`);
|
||||
const blob = new Blob([typeof json === "string" ? json : JSON.stringify(json, null, 2)], { type: "application/json" });
|
||||
const link = erstelleElement("a", { href: URL.createObjectURL(blob) });
|
||||
link.download = `automation-${p.id}.n8n.json`;
|
||||
link.click();
|
||||
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr", 8000);
|
||||
}
|
||||
};
|
||||
|
||||
return erstelleElement("div", { klasse: "automation" },
|
||||
erstelleElement("strong", { text: `#${p.id} ${p.purpose}` }),
|
||||
erstelleElement("div", { klasse: "gedaempft text-klein", text: `${p.agentId} · ${formatiereDatumZeit(p.createdAt)} · ${t("automation.trigger")}: ${p.trigger}` }),
|
||||
erstelleElement("p", { klasse: "text-klein",
|
||||
text: t("automation.savings", { days: p.days, runs: p.runs, cost: p.costUsd.toFixed(2), month: p.costPerMonthUsd.toFixed(2), runsMonth: p.runsPerMonth }) }),
|
||||
erstelleElement("p", { klasse: "text-klein", text: `${t("automation.does")}: ${p.summary}` }),
|
||||
erstelleElement("table", { klasse: "daten-tabelle" },
|
||||
erstelleElement("tbody", {}, ...p.nodes.map((n) => erstelleElement("tr", { klasse: n.dangerous ? "org-ausreisser" : "" },
|
||||
erstelleElement("td", { text: n.name }),
|
||||
erstelleElement("td", { text: n.effect }),
|
||||
erstelleElement("td", { klasse: "gedaempft", text: n.target ?? "" }))))),
|
||||
...p.warnings.map((w) => erstelleElement("p", { klasse: "hinweis-box warnung text-klein", text: w })),
|
||||
erstelleElement("dl", { klasse: "datenliste" },
|
||||
erstelleElement("dt", { text: t("automation.replaces") }), erstelleElement("dd", { text: p.taskIds.join(", ") }),
|
||||
erstelleElement("dt", { text: t("automation.services") }), erstelleElement("dd", { text: p.services.join(", ") || "—" }),
|
||||
erstelleElement("dt", { text: t("automation.risks") }), erstelleElement("dd", { text: p.risks }),
|
||||
erstelleElement("dt", { text: t("automation.testGuide") }), erstelleElement("dd", { text: p.testGuide })),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" },
|
||||
erstelleElement("button", { type: "button", klasse: "klein", text: t("automation.download"), onclick: herunterladen }),
|
||||
grund,
|
||||
erstelleElement("button", { type: "button", klasse: "primaer klein", text: t("automation.accept"), onclick: () => entscheiden(true) }),
|
||||
erstelleElement("button", { type: "button", klasse: "klein", text: t("automation.reject"), onclick: () => entscheiden(false) })));
|
||||
}
|
||||
|
||||
lade();
|
||||
return erstelleKarte(t("automation.title"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("automation.hint") }),
|
||||
box
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user