feat(agent): 触发器新增「工具调用」条件 + 定时触发改为复选框展开

- 定时触发统一为前置复选框,勾选后展开「每 N 秒」+ 消息(与其它条件一致)
- 新增工具调用触发(P3 第 5 种条件):
  - 选中系统工具列表(至少一个),任务执行中这些工具每次调用完成即触发 agent
  - 触发消息带上任务 id/描述/目标 + 工具名 + 入参 + 返回内容(各截断 1500 字)
  - 只读任务执行的 activity,触发跑的会话写 conversation_activities,天然无自触发循环
- DB: agent_triggers 加 on_tool_call/tool_call_message/tool_names 三列;
  已发版,附幂等 ALTER TABLE ADD COLUMN IF NOT EXISTS 供旧库升级补列
- scheduler: 新增 last_toolcall_id 水位线(首启 seed) + fireToolCalls,以 tool_result
  为水位、回连 tool_use 取入参,按选中工具集合匹配后触发

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Autumn-27andClaude Opus 4.8 committed 2026-08-08 02:59:07 -04:00
1 parent 82bd19a1cd
commit f9716269ea
6 files changed
+275 -51

No files matched your search

+83 -14
View File
@@ -688,18 +688,23 @@ function DiffView({ oldText, newText }: { oldText: string; newText: string }) {
}
// AgentTriggersTab manages a custom agent's P3 triggers: list + add + delete.
// Each trigger fires (定时/发现finding/目标达成,可多选) → a new conversation runs
// Each trigger fires (定时/发现finding/目标达成/任务超时/工具调用,可多选) → a new conversation runs
// in parallel with the base user message + auto context appended by the backend.
function AgentTriggersTab({ agentKey }: { agentKey: string }) {
const [triggers, setTriggers] = React.useState<AgentTrigger[]>([]);
const [intervalSec, setIntervalSec] = React.useState("0");
const [tools, setTools] = React.useState<Tool[]>([]);
const [onInterval, setOnInterval] = React.useState(false);
const [intervalSec, setIntervalSec] = React.useState("60");
const [onFinding, setOnFinding] = React.useState(false);
const [onGoalMet, setOnGoalMet] = React.useState(false);
const [onTaskTimeout, setOnTaskTimeout] = React.useState(false);
const [onToolCall, setOnToolCall] = React.useState(false);
const [intervalMsg, setIntervalMsg] = React.useState("");
const [findingMsg, setFindingMsg] = React.useState("");
const [goalMsg, setGoalMsg] = React.useState("");
const [taskTimeoutMsg, setTaskTimeoutMsg] = React.useState("");
const [toolCallMsg, setToolCallMsg] = React.useState("");
const [toolNames, setToolNames] = React.useState<string[]>([]);
const [saving, setSaving] = React.useState(false);
const reload = React.useCallback(() => {
@@ -708,13 +713,24 @@ function AgentTriggersTab({ agentKey }: { agentKey: string }) {
React.useEffect(() => {
reload();
}, [reload]);
React.useEffect(() => {
api.tools().then(setTools).catch(() => setTools([]));
}, []);
function toggleTool(key: string) {
setToolNames((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]));
}
async function create() {
const n = Math.max(0, Math.floor(Number(intervalSec) || 0));
if (n === 0 && !onFinding && !onGoalMet && !onTaskTimeout) {
const n = onInterval ? Math.max(1, Math.floor(Number(intervalSec) || 0)) : 0;
if (n === 0 && !onFinding && !onGoalMet && !onTaskTimeout && !onToolCall) {
toast.error("至少选择一种触发条件");
return;
}
if (onToolCall && toolNames.length === 0) {
toast.error("工具调用触发至少选择一个工具");
return;
}
setSaving(true);
try {
await api.createTrigger(agentKey, {
@@ -723,20 +739,27 @@ function AgentTriggersTab({ agentKey }: { agentKey: string }) {
on_finding: onFinding,
on_goal_met: onGoalMet,
on_task_timeout: onTaskTimeout,
on_tool_call: onToolCall,
interval_message: intervalMsg.trim(),
finding_message: findingMsg.trim(),
goal_message: goalMsg.trim(),
task_timeout_message: taskTimeoutMsg.trim(),
tool_call_message: toolCallMsg.trim(),
tool_names: onToolCall ? toolNames : [],
});
toast.success("已添加触发器");
setIntervalSec("0");
setOnInterval(false);
setIntervalSec("60");
setOnFinding(false);
setOnGoalMet(false);
setOnTaskTimeout(false);
setOnToolCall(false);
setIntervalMsg("");
setFindingMsg("");
setGoalMsg("");
setTaskTimeoutMsg("");
setToolCallMsg("");
setToolNames([]);
reload();
} catch (e) {
toast.error("添加失败:" + (e as Error).message);
@@ -752,10 +775,13 @@ function AgentTriggersTab({ agentKey }: { agentKey: string }) {
on_finding: t.on_finding,
on_goal_met: t.on_goal_met,
on_task_timeout: t.on_task_timeout,
on_tool_call: t.on_tool_call,
interval_message: t.interval_message,
finding_message: t.finding_message,
goal_message: t.goal_message,
task_timeout_message: t.task_timeout_message,
tool_call_message: t.tool_call_message,
tool_names: t.tool_names,
});
reload();
} catch (e) {
@@ -777,6 +803,7 @@ function AgentTriggersTab({ agentKey }: { agentKey: string }) {
if (t.on_finding) parts.push("发现 finding");
if (t.on_goal_met) parts.push("目标达成");
if (t.on_task_timeout) parts.push("任务超时");
if (t.on_tool_call) parts.push(`工具调用(${t.tool_names.length})`);
return parts.join(" · ") || "(无条件)";
}
@@ -793,15 +820,20 @@ function AgentTriggersTab({ agentKey }: { agentKey: string }) {
{/* 定时 */}
<div className="grid gap-1.5">
<div className="flex items-center gap-2">
<Label htmlFor="tr-interval" className="text-xs">定时触发:每</Label>
<Input id="tr-interval" type="number" min={0} className="h-8 w-24"
value={intervalSec} onChange={(e) => setIntervalSec(e.target.value)} />
<span className="text-muted-foreground text-xs">秒(0=关)</span>
</div>
{Number(intervalSec) > 0 && (
<Textarea className="text-xs" rows={2} value={intervalMsg}
placeholder="定时触发时发给 agent 的话,如:巡检所有任务" onChange={(e) => setIntervalMsg(e.target.value)} />
<label className="flex items-center gap-2 text-sm">
<Checkbox checked={onInterval} onCheckedChange={(v) => setOnInterval(!!v)} /> 定时触发
</label>
{onInterval && (
<div className="grid gap-1.5">
<div className="flex items-center gap-2">
<Label htmlFor="tr-interval" className="text-xs">每</Label>
<Input id="tr-interval" type="number" min={1} className="h-8 w-24"
value={intervalSec} onChange={(e) => setIntervalSec(e.target.value)} />
<span className="text-muted-foreground text-xs">秒</span>
</div>
<Textarea className="text-xs" rows={2} value={intervalMsg}
placeholder="定时触发时发给 agent 的话,如:巡检所有任务" onChange={(e) => setIntervalMsg(e.target.value)} />
</div>
)}
</div>
@@ -838,6 +870,37 @@ function AgentTriggersTab({ agentKey }: { agentKey: string }) {
)}
</div>
{/* 工具调用 */}
<div className="grid gap-1.5">
<label className="flex items-center gap-2 text-sm">
<Checkbox checked={onToolCall} onCheckedChange={(v) => setOnToolCall(!!v)} /> 工具调用时触发
</label>
{onToolCall && (
<div className="grid gap-1.5">
<div className="text-muted-foreground text-xs">
选择要监听的工具(至少一个);任务执行中这些工具每次<b>调用完成</b>都会触发。已选 {toolNames.length} 个。
</div>
<div className="max-h-40 overflow-y-auto rounded-md border p-2">
{tools.length === 0 && <span className="text-muted-foreground text-xs">(工具列表为空)</span>}
<div className="grid gap-1">
{tools.map((tool) => (
<label key={tool.key} className="flex items-start gap-2 text-xs">
<Checkbox className="mt-0.5" checked={toolNames.includes(tool.key)}
onCheckedChange={() => toggleTool(tool.key)} />
<span className="min-w-0">
<span className="font-medium">{tool.key}</span>
{tool.description && <span className="text-muted-foreground line-clamp-1"> {tool.description}</span>}
</span>
</label>
))}
</div>
</div>
<Textarea className="text-xs" rows={2} value={toolCallMsg}
placeholder="工具被调用时发给 agent 的话(系统会附带任务信息、工具入参与返回内容)" onChange={(e) => setToolCallMsg(e.target.value)} />
</div>
)}
</div>
<div>
<Button size="sm" onClick={create} disabled={saving}>
<SaveIcon /> 添加触发器
@@ -864,6 +927,12 @@ function AgentTriggersTab({ agentKey }: { agentKey: string }) {
{t.on_finding && t.finding_message && <div className="line-clamp-1">finding:{t.finding_message}</div>}
{t.on_goal_met && t.goal_message && <div className="line-clamp-1">目标:{t.goal_message}</div>}
{t.on_task_timeout && t.task_timeout_message && <div className="line-clamp-1">超时:{t.task_timeout_message}</div>}
{t.on_tool_call && (
<>
<div className="line-clamp-1">工具:{t.tool_names.join("、") || "(未选)"}</div>
{t.tool_call_message && <div className="line-clamp-1">消息:{t.tool_call_message}</div>}
</>
)}
</div>
</div>
<Button variant="ghost" size="icon-sm" className="text-muted-foreground hover:text-destructive"
+3
View File
@@ -227,10 +227,13 @@ export interface AgentTrigger {
on_finding: boolean; // 任意任务发现 finding 时触发
on_goal_met: boolean; // 任意任务达成目标时触发
on_task_timeout: boolean; // 任意任务超时时触发
on_tool_call: boolean; // 选中工具被调用(执行完成)时触发
interval_message: string; // 各触发条件的独立用户消息
finding_message: string;
goal_message: string;
task_timeout_message: string;
tool_call_message: string;
tool_names: string[]; // on_tool_call 选中的工具 key(至少一个)
last_fire?: string;
}