Trigger: Auftrag, Adressname, Prioritaet, Bearbeiten und neues Token (#725)
Rueckmeldung aus der Testinstanz: Nach dem Anlegen war nicht zu erkennen,
wofuer ein Trigger da ist, der Agent erfuhr nur 'ein Ereignis kam', und
bearbeiten ging nicht.
- Slug aus dem Namen in der Adresse: /hooks/<instanz>/trigger/<slug>;
ein Token an fremder Adresse oeffnet nichts. Die alte Adresse ohne
Slug bleibt gueltig.
- Auftrag vom Menschen steht vor den gelieferten Daten (die weiter als
Fremdinhalt gerahmt sind); ohne Auftrag wird der Agent darauf hingewiesen.
- Prioritaet der erzeugten Aufgaben.
- PUT /api/triggers/{id} (Token bleibt), POST /api/triggers/{id}/token.
- WebUI: Formular fuer Anlegen und Bearbeiten, Adresse und Auftrag je Zeile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
44613c2b77
commit
e2344f4d33
@@ -474,6 +474,24 @@ internal static class ManagerEndpoints
|
||||
await forwarding.ForwardMcpAsync(context, ziel, ct, "/hooks/trigger");
|
||||
});
|
||||
|
||||
// The same with the trigger's name in the address — readable for whoever sets up the sender.
|
||||
app.MapPost("/hooks/{instanz}/trigger/{name}", async (
|
||||
HttpContext context, string instanz, string name, InstanceRegistry registry, InstanceForwarding forwarding,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (context.Request.Headers.Origin.Count > 0)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
return;
|
||||
}
|
||||
if (Find(registry, instanz) is not { } ziel)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
return;
|
||||
}
|
||||
await forwarding.ForwardMcpAsync(context, ziel, ct, "/hooks/trigger/" + Uri.EscapeDataString(name));
|
||||
});
|
||||
|
||||
app.Map("/api/{**pfad}", async (
|
||||
HttpContext context, InstanceRegistry registry, InstanceForwarding forwarding,
|
||||
CancellationToken ct) =>
|
||||
|
||||
@@ -1049,6 +1049,22 @@ button.klein {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* #725: a trigger shows where it is called and what the agent is told to do. */
|
||||
.trigger-adresse {
|
||||
font-family: var(--schrift-mono, monospace);
|
||||
font-size: 12px;
|
||||
color: var(--gedaempft);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.trigger-auftrag {
|
||||
margin: 6px 0;
|
||||
padding: 6px 10px;
|
||||
border-left: 3px solid var(--accent);
|
||||
white-space: pre-wrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── Ansicht: Aufgaben & Jobs ─── */
|
||||
|
||||
.reiter-leiste {
|
||||
|
||||
@@ -1140,16 +1140,38 @@
|
||||
"mcpServers.enable": "Einschalten",
|
||||
"mcpServers.remove": "Entfernen",
|
||||
"trigger.title": "Eingehende Auslöser (n8n und andere)",
|
||||
"trigger.hint": "Etwas von außen startet Arbeit für diesen Agenten: ein n8n-Workflow auf eine Mail, einen Zeitplan oder ein Formular — oder jedes System, das eine HTTP-Anfrage schickt. Jede Lieferung wird eine Aufgabe für diesen Agenten; ihr Inhalt ist Fremdinhalt, nie Anweisung. Ein Token je Auslöser, mit Obergrenze je Stunde.",
|
||||
"trigger.hint": "Etwas von außen startet Arbeit für diesen Agenten: ein n8n-Workflow auf eine Mail, einen Zeitplan oder ein Formular — oder jedes System, das eine HTTP-Anfrage schickt. Jede Lieferung wird eine Aufgabe mit dem Auftrag des Auslösers; was geliefert wird, ist Fremdinhalt, nie Anweisung. Ein Token je Auslöser, mit Obergrenze je Stunde.",
|
||||
"trigger.labelPlaceholder": "z. B. n8n neue Bestellungen",
|
||||
"trigger.perHour": "Höchstens je Stunde",
|
||||
"trigger.issue": "Auslöser anlegen",
|
||||
"trigger.none": "Keine Auslöser für diesen Agenten.",
|
||||
"trigger.status": "höchstens {perHour} je Stunde · {received} empfangen · zuletzt {last}",
|
||||
"trigger.status": "höchstens {perHour} je Stunde · Priorität {priority} · {received} empfangen · zuletzt {last}",
|
||||
"trigger.issuedTitle": "Neuer Auslöser für {agent}",
|
||||
"trigger.address": "Adresse",
|
||||
"trigger.example": "Beispiel",
|
||||
"trigger.exampleHint": "In n8n: ein HTTP-Request-Knoten, Methode POST, diese Adresse, Kopfzeile Authorization: Bearer und das Token. Ein JSON-Rumpf mit \"title\" oder \"subject\" benennt die Aufgabe.",
|
||||
"trigger.name": "Name",
|
||||
"trigger.instruction": "Auftrag an den Agenten",
|
||||
"trigger.instructionHint": "Was der Agent mit jeder Lieferung tut — z. B. „Prüfe die Bestellung gegen den Lagerbestand, trage sie unter bestellungen/ ein und melde dem Kundendienst, wenn etwas fehlt.“ Dieser Text kommt von dir und gilt als vertrauenswürdig; die gelieferten Daten nicht.",
|
||||
"trigger.instructionPlaceholder": "Was soll der Agent mit jedem Ereignis tun?",
|
||||
"trigger.noInstruction": "Kein Auftrag — der Agent erfährt nur, dass ein Ereignis kam, und muss raten, was zu tun ist.",
|
||||
"trigger.slug": "Adressname",
|
||||
"trigger.slugPlaceholder": "aus dem Namen, z. B. neue-bestellungen",
|
||||
"trigger.slugHint": "Steht in der Adresse, damit man beim Absender sieht, wofür der Auslöser ist. Leer: wird aus dem Namen gebildet.",
|
||||
"trigger.slugEditHint": "Eine Änderung ändert die Adresse — der Absender muss angepasst werden. Das Token bleibt.",
|
||||
"trigger.priority": "Priorität seiner Aufgaben",
|
||||
"trigger.priority5": "5 – am höchsten",
|
||||
"trigger.priority4": "4 – hoch",
|
||||
"trigger.priority3": "3 – normal",
|
||||
"trigger.priority2": "2 – niedrig",
|
||||
"trigger.priority1": "1 – am niedrigsten",
|
||||
"trigger.createTitle": "Neuer Auslöser für {agent}",
|
||||
"trigger.editTitle": "Auslöser {name} bearbeiten",
|
||||
"trigger.saved": "Auslöser gespeichert.",
|
||||
"trigger.addressChanged": "Auslöser gespeichert. Seine Adresse lautet jetzt {address} — passe den Absender an.",
|
||||
"trigger.newToken": "Neues Token",
|
||||
"trigger.newTokenConfirm": "Neues Token für „{name}“ ausgeben? Das alte gilt ab sofort nicht mehr.",
|
||||
"trigger.newTokenTitle": "Neues Token für {name}",
|
||||
|
||||
"runtime.title": "Laufzeit",
|
||||
"runtime.hint": "Native Läufe nutzen unsere Engine mit OpenRouter. Eine fremde Laufzeit arbeitet in der Sandbox auf einer Kopie des Arbeitsbereichs: Was sie ändert, kommt als Vorschlag zurück und wird nie von selbst übernommen. Taskboard und Kollegen erreicht sie nur über den MCP-Server dieser Instanz.",
|
||||
|
||||
@@ -1140,16 +1140,38 @@
|
||||
"mcpServers.enable": "Switch on",
|
||||
"mcpServers.remove": "Remove",
|
||||
"trigger.title": "Inbound triggers (n8n and others)",
|
||||
"trigger.hint": "Something outside starts work for this agent: an n8n workflow on a mail, a schedule or a form — or any system that sends an HTTP request. Each delivery becomes a task for this agent; its content is data from outside, never instructions. One token per trigger, capped per hour.",
|
||||
"trigger.hint": "Something outside starts work for this agent: an n8n workflow on a mail, a schedule or a form — or any system that sends an HTTP request. Each delivery becomes a task with the trigger's instruction; what arrives is data from outside, never instructions. One token per trigger, capped per hour.",
|
||||
"trigger.labelPlaceholder": "e.g. n8n new orders",
|
||||
"trigger.perHour": "Per hour at most",
|
||||
"trigger.issue": "Create trigger",
|
||||
"trigger.none": "No triggers for this agent.",
|
||||
"trigger.status": "at most {perHour} an hour · {received} received · last {last}",
|
||||
"trigger.status": "at most {perHour} an hour · priority {priority} · {received} received · last {last}",
|
||||
"trigger.issuedTitle": "New trigger for {agent}",
|
||||
"trigger.address": "Address",
|
||||
"trigger.example": "Example",
|
||||
"trigger.exampleHint": "In n8n: an HTTP Request node, method POST, this address, header Authorization: Bearer and the token. A JSON body with \"title\" or \"subject\" names the task.",
|
||||
"trigger.name": "Name",
|
||||
"trigger.instruction": "Instruction for the agent",
|
||||
"trigger.instructionHint": "What the agent does with every delivery — e.g. \"Check the order against stock, book it in orders/ and tell the customer desk if something is missing.\" This text comes from you and is trusted; the delivered data is not.",
|
||||
"trigger.instructionPlaceholder": "What should the agent do with each event?",
|
||||
"trigger.noInstruction": "No instruction — the agent only learns that an event came in and must guess what to do.",
|
||||
"trigger.slug": "Address name",
|
||||
"trigger.slugPlaceholder": "from the name, e.g. new-orders",
|
||||
"trigger.slugHint": "Appears in the address, so the sender shows what the trigger is for. Empty: made from the name.",
|
||||
"trigger.slugEditHint": "Changing it changes the address — the sender must be updated. The token stays.",
|
||||
"trigger.priority": "Priority of its tasks",
|
||||
"trigger.priority5": "5 – highest",
|
||||
"trigger.priority4": "4 – high",
|
||||
"trigger.priority3": "3 – normal",
|
||||
"trigger.priority2": "2 – low",
|
||||
"trigger.priority1": "1 – lowest",
|
||||
"trigger.createTitle": "New trigger for {agent}",
|
||||
"trigger.editTitle": "Edit trigger {name}",
|
||||
"trigger.saved": "Trigger saved.",
|
||||
"trigger.addressChanged": "Trigger saved. Its address is now {address} — update the sender.",
|
||||
"trigger.newToken": "New token",
|
||||
"trigger.newTokenConfirm": "Issue a new token for '{name}'? The old one stops working at once.",
|
||||
"trigger.newTokenTitle": "New token for {name}",
|
||||
|
||||
"runtime.title": "Runtime",
|
||||
"runtime.hint": "Native runs use our engine with OpenRouter. A foreign runtime works in the sandbox on a copy of the workspace: what it changes comes back as a proposal and is never applied by itself. It reaches the taskboard and colleagues only through this instance's MCP server.",
|
||||
|
||||
@@ -156,34 +156,22 @@ function zeigeNeuesToken(neu) {
|
||||
|
||||
/**
|
||||
* #725: Inbound triggers — n8n or any system that sends a request hands this agent a task.
|
||||
* One token per trigger, shown once; each delivery becomes a task whose content is framed as
|
||||
* foreign content. Nothing goes back: ClawdDotNet gets no right to change workflows.
|
||||
* Each trigger has a name in its address and an instruction a human wrote: what the agent does
|
||||
* with every delivery. What arrives stays data from outside; only the instruction is trusted.
|
||||
* One token per trigger, shown once; nothing goes back — ClawdDotNet gets no right to change workflows.
|
||||
*/
|
||||
export function erstelleTriggerKarte(agent) {
|
||||
const liste = erstelleElement("div", { klasse: "mcp-token-liste" });
|
||||
const bezeichnung = erstelleElement("input", { type: "text", placeholder: t("trigger.labelPlaceholder"), maxlength: "60" });
|
||||
const proStunde = erstelleElement("input", { type: "number", min: "1", max: "600", step: "1", value: "30", "aria-label": t("trigger.perHour") });
|
||||
|
||||
const ausgeben = erstelleElement("button", {
|
||||
const anlegen = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: t("trigger.issue"),
|
||||
onclick: async () => {
|
||||
ausgeben.disabled = true;
|
||||
try {
|
||||
const neu = await api("/triggers", {
|
||||
method: "POST",
|
||||
body: { agentId: agent.agentId, label: bezeichnung.value.trim(), perHour: Number(proStunde.value) }
|
||||
});
|
||||
bezeichnung.value = "";
|
||||
zeigeNeuenAusloeser(neu);
|
||||
await lade();
|
||||
} catch (err) {
|
||||
zeigeMeldung(t("mcp.failed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
ausgeben.disabled = false;
|
||||
}
|
||||
}
|
||||
onclick: () => zeigeTriggerFormular(agent, null, async (eingabe) => {
|
||||
const neu = await api("/triggers", { method: "POST", body: eingabe });
|
||||
zeigeNeuenAusloeser(neu, t("trigger.issuedTitle", { agent: neu.info.agentId }));
|
||||
await lade();
|
||||
})
|
||||
});
|
||||
|
||||
async function lade() {
|
||||
@@ -198,10 +186,15 @@ export function erstelleTriggerKarte(agent) {
|
||||
}
|
||||
}
|
||||
|
||||
function knopf(text, onclick) {
|
||||
return erstelleElement("button", { type: "button", klasse: "schaltflaeche flach klein", text, onclick });
|
||||
}
|
||||
|
||||
function zeile(trigger) {
|
||||
const status = trigger.active
|
||||
? t("trigger.status", {
|
||||
perHour: trigger.perHour,
|
||||
priority: trigger.priority,
|
||||
received: trigger.received,
|
||||
last: trigger.lastUsedAt ? formatiereDatumZeit(trigger.lastUsedAt) : "—"
|
||||
})
|
||||
@@ -209,11 +202,23 @@ export function erstelleTriggerKarte(agent) {
|
||||
|
||||
const aktionen = erstelleElement("div", { klasse: "werkzeug-aktionen" });
|
||||
if (trigger.active) {
|
||||
aktionen.append(erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach klein",
|
||||
text: t("mcp.revoke"),
|
||||
onclick: async () => {
|
||||
aktionen.append(
|
||||
knopf(t("common.edit"), () => zeigeTriggerFormular(agent, trigger, async (eingabe) => {
|
||||
const neu = await api(`/triggers/${encodeURIComponent(trigger.id)}`, { method: "PUT", body: eingabe });
|
||||
zeigeMeldung(neu.slug !== trigger.slug ? t("trigger.addressChanged", { address: triggerAdresse(neu.slug) }) : t("trigger.saved"),
|
||||
neu.slug !== trigger.slug ? "warnung" : "erfolg", neu.slug !== trigger.slug ? 10000 : 4000);
|
||||
await lade();
|
||||
})),
|
||||
knopf(t("trigger.newToken"), async () => {
|
||||
if (!confirm(t("trigger.newTokenConfirm", { name: trigger.label || trigger.slug }))) return;
|
||||
try {
|
||||
const neu = await api(`/triggers/${encodeURIComponent(trigger.id)}/token`, { method: "POST" });
|
||||
zeigeNeuenAusloeser(neu, t("trigger.newTokenTitle", { name: neu.info.label || neu.info.slug }));
|
||||
} catch (err) {
|
||||
zeigeMeldung(t("mcp.failed", { error: err.message }), "gefahr");
|
||||
}
|
||||
}),
|
||||
knopf(t("mcp.revoke"), async () => {
|
||||
try {
|
||||
await api(`/triggers/${encodeURIComponent(trigger.id)}`, { method: "DELETE" });
|
||||
zeigeMeldung(t("mcp.revokedMessage"), "info");
|
||||
@@ -221,13 +226,18 @@ export function erstelleTriggerKarte(agent) {
|
||||
} catch (err) {
|
||||
zeigeMeldung(t("mcp.failed", { error: err.message }), "gefahr");
|
||||
}
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
const auftrag = trigger.instruction
|
||||
? erstelleElement("div", { klasse: "trigger-auftrag", text: trigger.instruction })
|
||||
: erstelleElement("div", { klasse: "werkzeug-beschreibung text-klein", text: `⚠ ${t("trigger.noInstruction")}` });
|
||||
|
||||
return erstelleElement("div", { klasse: `werkzeug-zeile${trigger.active ? "" : " mcp-token-inaktiv"}` },
|
||||
erstelleElement("div", { klasse: "werkzeug-info" },
|
||||
erstelleElement("strong", { text: trigger.label || trigger.id }),
|
||||
erstelleElement("strong", { text: trigger.label || trigger.slug }),
|
||||
erstelleElement("div", { klasse: "trigger-adresse", text: triggerAdresse(trigger.slug) }),
|
||||
auftrag,
|
||||
erstelleElement("div", { klasse: "werkzeug-beschreibung gedaempft", text: status })),
|
||||
aktionen);
|
||||
}
|
||||
@@ -237,14 +247,71 @@ export function erstelleTriggerKarte(agent) {
|
||||
return erstelleKarte(t("trigger.title"), [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: t("trigger.hint") }),
|
||||
liste,
|
||||
erstelleElement("div", { klasse: "mcp-token-formular" }, bezeichnung, proStunde, ausgeben)
|
||||
erstelleElement("div", { klasse: "mcp-token-formular" }, anlegen)
|
||||
]);
|
||||
}
|
||||
|
||||
/** The trigger's token and address once — with how n8n (or anything else) calls it. */
|
||||
function zeigeNeuenAusloeser(neu) {
|
||||
/** The address a sender posts to — with the trigger's name, so whoever sets it up sees what it is for. */
|
||||
function triggerAdresse(slug) {
|
||||
const instanz = holeAktiveInstanz();
|
||||
const adresse = `${location.origin}/hooks/${instanz ? `${encodeURIComponent(instanz)}/` : ""}trigger`;
|
||||
return `${location.origin}/hooks/${instanz ? `${encodeURIComponent(instanz)}/` : ""}trigger/${encodeURIComponent(slug)}`;
|
||||
}
|
||||
|
||||
/** Create or edit a trigger: name, address name, the instruction for the agent, priority, rate. */
|
||||
function zeigeTriggerFormular(agent, trigger, speichern) {
|
||||
const name = erstelleElement("input", { type: "text", maxlength: "60", placeholder: t("trigger.labelPlaceholder"), value: trigger?.label ?? "" });
|
||||
const slug = erstelleElement("input", { type: "text", maxlength: "40", placeholder: t("trigger.slugPlaceholder"), value: trigger?.slug ?? "" });
|
||||
const auftrag = erstelleElement("textarea", { rows: "6", maxlength: "4000", placeholder: t("trigger.instructionPlaceholder") });
|
||||
auftrag.value = trigger?.instruction ?? "";
|
||||
const prioritaet = erstelleElement("select", {},
|
||||
...[5, 4, 3, 2, 1].map((p) => erstelleElement("option", { value: String(p), text: t(`trigger.priority${p}`) })));
|
||||
prioritaet.value = String(trigger?.priority ?? 3);
|
||||
const proStunde = erstelleElement("input", { type: "number", min: "1", max: "600", step: "1", value: String(trigger?.perHour ?? 30) });
|
||||
|
||||
const formular = erstelleElement("div", { klasse: "formular-raster" },
|
||||
erstelleElement("label", { text: t("trigger.name") }), name,
|
||||
erstelleElement("label", { text: t("trigger.instruction") }),
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("trigger.instructionHint") }),
|
||||
auftrag,
|
||||
erstelleElement("label", { text: t("trigger.slug") }),
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t(trigger ? "trigger.slugEditHint" : "trigger.slugHint") }),
|
||||
slug,
|
||||
erstelleElement("label", { text: t("trigger.priority") }), prioritaet,
|
||||
erstelleElement("label", { text: t("trigger.perHour") }), proStunde);
|
||||
|
||||
const abbrechen = erstelleElement("button", { type: "button", klasse: "schaltflaeche flach", text: t("common.cancel"), onclick: () => modal.schliessen() });
|
||||
const sichern = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: trigger ? t("common.save") : t("trigger.issue"),
|
||||
onclick: async () => {
|
||||
sichern.disabled = true;
|
||||
try {
|
||||
await speichern({
|
||||
agentId: agent.agentId,
|
||||
label: name.value.trim(),
|
||||
slug: slug.value.trim() || null,
|
||||
instruction: auftrag.value.trim(),
|
||||
priority: Number(prioritaet.value),
|
||||
perHour: Number(proStunde.value)
|
||||
});
|
||||
modal.schliessen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(t("mcp.failed", { error: err.message }), "gefahr");
|
||||
} finally {
|
||||
sichern.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const titel = trigger ? t("trigger.editTitle", { name: trigger.label || trigger.slug }) : t("trigger.createTitle", { agent: agent.agentId });
|
||||
const modal = erstelleModal(titel, formular, [abbrechen, sichern]);
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
/** The trigger's token and address once — with how n8n (or anything else) calls it. */
|
||||
function zeigeNeuenAusloeser(neu, titel) {
|
||||
const adresse = triggerAdresse(neu.info.slug);
|
||||
const beispiel = `curl -X POST "${adresse}" \\\n -H "Authorization: Bearer ${neu.token}" \\\n -H "Content-Type: application/json" \\\n -d '{"title":"New order","order":"4711"}'`;
|
||||
|
||||
const feld = (wert, zeilen) => {
|
||||
@@ -270,6 +337,6 @@ function zeigeNeuenAusloeser(neu) {
|
||||
text: t("common.close"),
|
||||
onclick: () => modal.schliessen()
|
||||
});
|
||||
const modal = erstelleModal(t("trigger.issuedTitle", { agent: neu.info.agentId }), inhalt, [fertig]);
|
||||
const modal = erstelleModal(titel, inhalt, [fertig]);
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user