feat(agent): 触发后处理可配置(运行模式/合并模式/并发上限) + 修复抽屉内下拉误关
每个自定义 agent 可配置触发后如何调度运行: - 运行模式 serial|parallel:串行排队一次一个 / 每次触发各自并发一个会话 - 合并模式 by_task|all|none(仅 serial):同任务合并 / 全部合并成一条 / 不合并 - parallel 带每 agent 最大并发上限(0=不限),超限排队等空位 实现: - DB: agents 加 trigger_run_mode/trigger_merge_mode/trigger_max_parallel 三列 (schema + 幂等 ALTER 迁移,已发版补旧库);Agent 结构/scan/SetAgentTriggerBehavior - 调度重构: triggerRun bool → triggerActive 计数 + triggerCfg 缓存; StartTriggeredRun 走 pumpLocked/runAndPump 统一泵模型,nextTriggerRun 按合并模式 取(新增 mergeAllRuns);并发安全(session 按 conv-id 隔离,已确认) - config 接口暴露/接收三字段;max_turns 改可选指针,避免触发 tab 部分 patch 清零 - 前端: 触发 tab 顶部加运行/合并模式下拉 + 最大并发输入,改动即存 修复: 抽屉(Sheet)内 Select 下拉 portal 到抽屉外,点选项/点别处被当成点击外部 误关抽屉。SelectContent 改 position=popper,并给 SheetContent 加 onInteractOutside 守卫(按 e.detail.originalEvent.target 判定弹层内点击则不关)。 Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
f9716269ea
commit
6d8fa5bc48
10 files changed
+374
-98
No files matched your search
@@ -258,6 +258,15 @@ export default function AgentsPage() {
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="flex flex-col gap-0 p-0 data-[side=right]:w-[45vw] data-[side=right]:sm:max-w-[45vw]"
|
||||
// 抽屉内的下拉/弹出层(Select 等)会 portal 到抽屉外;交互源自这些弹出层时
|
||||
// 不要把它当成「点击外部」而关闭抽屉。注意:Radix 的 onInteractOutside 里
|
||||
// e.target 是层节点本身,真正被点的元素在 e.detail.originalEvent.target。
|
||||
onInteractOutside={(e) => {
|
||||
const t = e.detail.originalEvent.target as Element | null;
|
||||
if (t?.closest?.("[data-radix-popper-content-wrapper],[data-radix-select-content],[role='listbox']")) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{editing && (
|
||||
<>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { api } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { AgentDetail, AgentTrigger, MCPServer, PromptVar, PromptVersion, Settings, SkillItem, Tool } from "@/lib/types";
|
||||
import type { Agent, AgentDetail, AgentTrigger, MCPServer, PromptVar, PromptVersion, Settings, SkillItem, Tool } from "@/lib/types";
|
||||
|
||||
// Traffic tools are host tools gated by the global 流量捕获 switch: bindable, but
|
||||
// only usable when capture is on. Keep this list in sync with traffic.SeedToolMetas.
|
||||
@@ -626,7 +627,7 @@ export function AgentEditor({ agentKey, onSaved }: { agentKey: string; onSaved?:
|
||||
{/* 触发(P3, 仅自定义 agent) */}
|
||||
{isCustom && (
|
||||
<TabsContent value="triggers" className="min-h-0 flex-1 overflow-y-auto px-4 pb-4">
|
||||
<AgentTriggersTab agentKey={agentKey} />
|
||||
<AgentTriggersTab agentKey={agentKey} agent={detail?.agent} />
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
@@ -690,9 +691,30 @@ 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
|
||||
// in parallel with the base user message + auto context appended by the backend.
|
||||
function AgentTriggersTab({ agentKey }: { agentKey: string }) {
|
||||
function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent }) {
|
||||
const [triggers, setTriggers] = React.useState<AgentTrigger[]>([]);
|
||||
const [tools, setTools] = React.useState<Tool[]>([]);
|
||||
// 触发后处理策略(每 agent);初值来自 agent detail,改动即保存。
|
||||
const [runMode, setRunMode] = React.useState<"serial" | "parallel">(agent?.trigger_run_mode ?? "serial");
|
||||
const [mergeMode, setMergeMode] = React.useState<"by_task" | "all" | "none">(agent?.trigger_merge_mode ?? "by_task");
|
||||
const [maxParallel, setMaxParallel] = React.useState(String(agent?.trigger_max_parallel ?? 5));
|
||||
React.useEffect(() => {
|
||||
setRunMode(agent?.trigger_run_mode ?? "serial");
|
||||
setMergeMode(agent?.trigger_merge_mode ?? "by_task");
|
||||
setMaxParallel(String(agent?.trigger_max_parallel ?? 5));
|
||||
}, [agent?.trigger_run_mode, agent?.trigger_merge_mode, agent?.trigger_max_parallel]);
|
||||
|
||||
async function saveBehavior(patch: {
|
||||
trigger_run_mode?: "serial" | "parallel";
|
||||
trigger_merge_mode?: "by_task" | "all" | "none";
|
||||
trigger_max_parallel?: number;
|
||||
}) {
|
||||
try {
|
||||
await api.saveAgentConfig(agentKey, patch);
|
||||
} catch (e) {
|
||||
toast.error("保存策略失败:" + (e as Error).message);
|
||||
}
|
||||
}
|
||||
const [onInterval, setOnInterval] = React.useState(false);
|
||||
const [intervalSec, setIntervalSec] = React.useState("60");
|
||||
const [onFinding, setOnFinding] = React.useState(false);
|
||||
@@ -810,10 +832,86 @@ function AgentTriggersTab({ agentKey }: { agentKey: string }) {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
触发器让这个自定义 Agent 自动运行:每次触发都<b>新建一个会话并行运行</b>(在「对话」页可见)。
|
||||
触发器让这个自定义 Agent 自动运行:每次触发都<b>新建一个会话运行</b>(在「对话」页可见)。
|
||||
可多选触发条件;系统会把「本次为何触发 + 相关任务/finding/目标」自动附加到你写的基础消息后面。
|
||||
</p>
|
||||
|
||||
{/* 触发后处理策略 */}
|
||||
<div className="grid gap-3 rounded-md border p-3">
|
||||
<Label className="text-muted-foreground text-xs">触发后处理策略(决定触发如何排队/合并运行)</Label>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="grid gap-1">
|
||||
<Label className="text-xs">运行模式</Label>
|
||||
<Select
|
||||
value={runMode}
|
||||
onValueChange={(v) => {
|
||||
const rm = v as "serial" | "parallel";
|
||||
setRunMode(rm);
|
||||
saveBehavior({ trigger_run_mode: rm });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="sm" className="h-8 w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="serial">串行(排队,一次一个)</SelectItem>
|
||||
<SelectItem value="parallel">并行(各自并发会话)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label className="text-xs">合并模式</Label>
|
||||
<Select
|
||||
value={mergeMode}
|
||||
disabled={runMode === "parallel"}
|
||||
onValueChange={(v) => {
|
||||
const mm = v as "by_task" | "all" | "none";
|
||||
setMergeMode(mm);
|
||||
saveBehavior({ trigger_merge_mode: mm });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="sm" className="h-8 w-44">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="by_task">按任务合并</SelectItem>
|
||||
<SelectItem value="all">全部合并成一条</SelectItem>
|
||||
<SelectItem value="none">不合并</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{runMode === "parallel" && (
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="tr-maxpar" className="text-xs">最大并发(0=不限)</Label>
|
||||
<Input
|
||||
id="tr-maxpar"
|
||||
type="number"
|
||||
min={0}
|
||||
className="h-8 w-28"
|
||||
value={maxParallel}
|
||||
onChange={(e) => setMaxParallel(e.target.value)}
|
||||
onBlur={() => {
|
||||
const n = Math.max(0, Math.floor(Number(maxParallel) || 0));
|
||||
setMaxParallel(String(n));
|
||||
saveBehavior({ trigger_max_parallel: n });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{runMode === "parallel"
|
||||
? "并行:每次触发立即各开一个会话并发运行,不合并;超过最大并发的触发排队等空位。"
|
||||
: mergeMode === "by_task"
|
||||
? "串行·按任务合并:同 agent 一次跑一个;排队中同一任务的事件触发合并成一条会话。"
|
||||
: mergeMode === "all"
|
||||
? "串行·全部合并:同 agent 一次跑一个;取队列时把当前排队的所有触发合并成一条会话。"
|
||||
: "串行·不合并:同 agent 一次跑一个;每条触发各自一个会话。"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 新增触发器 */}
|
||||
<div className="grid gap-3 rounded-md border p-3">
|
||||
<Label className="text-muted-foreground text-xs">新增触发器(每种条件可填各自的用户消息)</Label>
|
||||
|
||||
+12
-2
@@ -315,8 +315,18 @@ export const api = {
|
||||
updateTrigger: (id: number, t: Omit<AgentTrigger, "id" | "agent_key" | "last_fire">) =>
|
||||
patch<{ ok: boolean }>(`/triggers/${id}`, t),
|
||||
deleteTrigger: (id: number) => del<{ deleted: number }>(`/triggers/${id}`),
|
||||
saveAgentConfig: (key: string, patch: { max_turns: number; run_seconds?: number; web_search?: boolean; interactive_shell?: boolean }) =>
|
||||
put<{ ok: boolean }>(`/agents/${key}/config`, patch),
|
||||
saveAgentConfig: (
|
||||
key: string,
|
||||
patch: {
|
||||
max_turns?: number;
|
||||
run_seconds?: number;
|
||||
web_search?: boolean;
|
||||
interactive_shell?: boolean;
|
||||
trigger_run_mode?: "serial" | "parallel";
|
||||
trigger_merge_mode?: "by_task" | "all" | "none";
|
||||
trigger_max_parallel?: number;
|
||||
},
|
||||
) => put<{ ok: boolean }>(`/agents/${key}/config`, patch),
|
||||
agentPromptVersions: (key: string) => get<{ versions: PromptVersion[] }>(`/agents/${key}/prompts`).then((r) => arr(r.versions)),
|
||||
agentVariables: (key: string) => get<{ variables: PromptVar[] }>(`/agents/${key}/variables`).then((r) => arr(r.variables)),
|
||||
previewAgentPrompt: (key: string, template: string, sample?: Record<string, string>) =>
|
||||
|
||||
@@ -387,6 +387,10 @@ export interface Agent {
|
||||
run_seconds?: number; // worker 单次运行墙钟上限(秒);0 = 无限制
|
||||
web_search?: boolean; // 是否启用网络搜索(受系统全局开关门控)
|
||||
interactive_shell?: boolean; // 是否启用交互式 shell(持久 PTY 会话工具族)
|
||||
// P3 触发后处理策略(仅自定义 agent 有意义)
|
||||
trigger_run_mode?: "serial" | "parallel"; // 串行排队 / 每次触发各自并发一个会话
|
||||
trigger_merge_mode?: "by_task" | "all" | "none"; // 仅 serial:同任务合并 / 全部合并 / 不合并
|
||||
trigger_max_parallel?: number; // 仅 parallel:每 agent 并发上限;0=不限
|
||||
// 绑定数量(仅列表接口返回):可见 MCP / 可见 Skill / 绑定工具
|
||||
mcp_count?: number;
|
||||
skill_count?: number;
|
||||
|
||||
Reference in new issue
Block a user