feat: 触发器支持编辑(前端补齐,后端早已支持 PATCH)

触发器 Tab 之前只能新增/删除/启停。改造「新增」表单为「新增/编辑」双模式:
- 每条触发器加「编辑」按钮 → 灌入表单进入编辑态(高亮该行),改完点「保存修改」
  调 api.updateTrigger;可「取消编辑」
- 编辑时保留该触发器原有启用状态;删除正在编辑的触发器会自动退出编辑态
后端 PATCH /api/triggers/{id} 与 api.updateTrigger 早已存在,本次仅补前端 UI。
This commit is contained in:
Autumn-27 committed 2026-08-18 12:38:14 -04:00
1 parent 87221be586
commit 57114a2994
1 file changed
+95 -41
+95 -41
View File
@@ -2,7 +2,7 @@
import * as React from "react";
import { toast } from "sonner";
import { EyeIcon, GitCompareIcon, InfoIcon, RotateCcwIcon, SaveIcon, Trash2Icon } from "lucide-react";
import { EyeIcon, GitCompareIcon, InfoIcon, PencilIcon, RotateCcwIcon, SaveIcon, Trash2Icon, XIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -768,6 +768,8 @@ function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent
const [taskCreateMsg, setTaskCreateMsg] = React.useState("");
const [toolNames, setToolNames] = React.useState<string[]>([]);
const [saving, setSaving] = React.useState(false);
// null = 新增模式;非 null = 正在编辑该 id 的触发器。
const [editingId, setEditingId] = React.useState<number | null>(null);
const reload = React.useCallback(() => {
api.agentTriggers(agentKey).then(setTriggers).catch(() => setTriggers([]));
@@ -783,7 +785,46 @@ function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent
setToolNames((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]));
}
async function create() {
// resetForm 清空表单并回到「新增」模式。
function resetForm() {
setEditingId(null);
setOnInterval(false);
setIntervalSec("60");
setOnFinding(false);
setOnGoalMet(false);
setOnTaskTimeout(false);
setOnToolCall(false);
setOnTaskCreate(false);
setIntervalMsg("");
setFindingMsg("");
setGoalMsg("");
setTaskTimeoutMsg("");
setToolCallMsg("");
setTaskCreateMsg("");
setToolNames([]);
}
// startEdit 把某条已有触发器灌进表单,进入「编辑」模式。
function startEdit(t: AgentTrigger) {
setEditingId(t.id);
setOnInterval(t.interval_sec > 0);
setIntervalSec(t.interval_sec > 0 ? String(t.interval_sec) : "60");
setOnFinding(t.on_finding);
setOnGoalMet(t.on_goal_met);
setOnTaskTimeout(t.on_task_timeout);
setOnToolCall(t.on_tool_call);
setOnTaskCreate(t.on_task_create);
setIntervalMsg(t.interval_message);
setFindingMsg(t.finding_message);
setGoalMsg(t.goal_message);
setTaskTimeoutMsg(t.task_timeout_message);
setToolCallMsg(t.tool_call_message);
setTaskCreateMsg(t.task_create_message);
setToolNames(t.tool_names ?? []);
}
// submit 依 editingId 走「新增」或「保存修改」;编辑时保留该触发器的启用状态。
async function submit() {
const n = onInterval ? Math.max(1, Math.floor(Number(intervalSec) || 0)) : 0;
if (n === 0 && !onFinding && !onGoalMet && !onTaskTimeout && !onToolCall && !onTaskCreate) {
toast.error("至少选择一种触发条件");
@@ -793,42 +834,35 @@ function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent
toast.error("工具调用触发至少选择一个工具");
return;
}
const body = {
interval_sec: n,
on_finding: onFinding,
on_goal_met: onGoalMet,
on_task_timeout: onTaskTimeout,
on_tool_call: onToolCall,
on_task_create: onTaskCreate,
interval_message: intervalMsg.trim(),
finding_message: findingMsg.trim(),
goal_message: goalMsg.trim(),
task_timeout_message: taskTimeoutMsg.trim(),
tool_call_message: toolCallMsg.trim(),
task_create_message: taskCreateMsg.trim(),
tool_names: onToolCall ? toolNames : [],
};
setSaving(true);
try {
await api.createTrigger(agentKey, {
enabled: true,
interval_sec: n,
on_finding: onFinding,
on_goal_met: onGoalMet,
on_task_timeout: onTaskTimeout,
on_tool_call: onToolCall,
on_task_create: onTaskCreate,
interval_message: intervalMsg.trim(),
finding_message: findingMsg.trim(),
goal_message: goalMsg.trim(),
task_timeout_message: taskTimeoutMsg.trim(),
tool_call_message: toolCallMsg.trim(),
task_create_message: taskCreateMsg.trim(),
tool_names: onToolCall ? toolNames : [],
});
toast.success("已添加触发器");
setOnInterval(false);
setIntervalSec("60");
setOnFinding(false);
setOnGoalMet(false);
setOnTaskTimeout(false);
setOnToolCall(false);
setOnTaskCreate(false);
setIntervalMsg("");
setFindingMsg("");
setGoalMsg("");
setTaskTimeoutMsg("");
setToolCallMsg("");
setTaskCreateMsg("");
setToolNames([]);
if (editingId != null) {
const cur = triggers.find((x) => x.id === editingId);
await api.updateTrigger(editingId, { ...body, enabled: cur?.enabled ?? true });
toast.success("已保存修改");
} else {
await api.createTrigger(agentKey, { ...body, enabled: true });
toast.success("已添加触发器");
}
resetForm();
reload();
} catch (e) {
toast.error("添加失败:" + (e as Error).message);
toast.error((editingId != null ? "保存失败:" : "添加失败:") + (e as Error).message);
} finally {
setSaving(false);
}
@@ -859,6 +893,7 @@ function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent
async function del(id: number) {
try {
await api.deleteTrigger(id);
if (editingId === id) resetForm();
reload();
} catch (e) {
toast.error("删除失败:" + (e as Error).message);
@@ -959,9 +994,13 @@ function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent
</p>
</div>
{/* 新增触发器 */}
{/* 新增 / 编辑触发器 */}
<div className="grid gap-3 rounded-md border p-3">
<Label className="text-muted-foreground text-xs">新增触发器(每种条件可填各自的用户消息)</Label>
<Label className="text-muted-foreground text-xs">
{editingId != null
? `编辑触发器 #${editingId}(改完点「保存修改」)`
: "新增触发器(每种条件可填各自的用户消息)"}
</Label>
{/* 定时 */}
<div className="grid gap-1.5">
@@ -1057,10 +1096,15 @@ function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent
)}
</div>
<div>
<Button size="sm" onClick={create} disabled={saving}>
<SaveIcon /> 添加触发器
<div className="flex items-center gap-2">
<Button size="sm" onClick={submit} disabled={saving}>
<SaveIcon /> {editingId != null ? "保存修改" : "添加触发器"}
</Button>
{editingId != null && (
<Button size="sm" variant="ghost" onClick={resetForm} disabled={saving}>
<XIcon /> 取消编辑
</Button>
)}
</div>
</div>
@@ -1069,7 +1113,13 @@ function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent
<Label className="text-muted-foreground text-xs">已有触发器</Label>
{triggers.length === 0 && <span className="text-muted-foreground text-xs">(暂无)</span>}
{triggers.map((t) => (
<div key={t.id} className="flex items-start gap-2 rounded-md border p-2 text-sm">
<div
key={t.id}
className={cn(
"flex items-start gap-2 rounded-md border p-2 text-sm",
editingId === t.id && "border-primary bg-primary/5",
)}
>
<Switch checked={t.enabled} onCheckedChange={() => toggleEnabled(t)} className="mt-0.5" />
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-1.5">
@@ -1092,8 +1142,12 @@ function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent
)}
</div>
</div>
<Button variant="ghost" size="icon-sm" className="text-muted-foreground hover:text-foreground"
onClick={() => startEdit(t)} title="编辑">
<PencilIcon className="size-3.5" />
</Button>
<Button variant="ghost" size="icon-sm" className="text-muted-foreground hover:text-destructive"
onClick={() => del(t.id)}>
onClick={() => del(t.id)} title="删除">
<Trash2Icon className="size-3.5" />
</Button>
</div>