diff --git a/agent/chat.go b/agent/chat.go index 6562bff..237fbef 100644 --- a/agent/chat.go +++ b/agent/chat.go @@ -57,14 +57,15 @@ func chatWorkDirSpec(workDir string) string { // chatSystem renders the DB-managed prompt body for agentKey. Custom agents have // no per-key in-code default, so DefaultAssistantPrompt is the render fallback. -func chatSystem(agentKey, workDir string) string { - return renderSystem(agentKey, DefaultAssistantPrompt, chatVars{Now: nowStr()}) + chatWorkDirSpec(workDir) +func chatSystem(agentKey, dataDir, workDir string) string { + return renderSystem(agentKey, DefaultAssistantPrompt, chatVars{DataDir: dataDir, Now: nowStr()}) + chatWorkDirSpec(workDir) } // chatVars carries the runtime variables a custom agent's prompt may reference. -// Now (server wall-clock, refreshed each turn) is the only one today; any other -// {{.X}} fails to render and falls back to DefaultAssistantPrompt. -type chatVars struct{ Now string } +// DataDir (server data root) + Now (server wall-clock, refreshed each turn) are the +// universal ones; any other {{.X}} fails to render and falls back to +// DefaultAssistantPrompt. +type chatVars struct{ DataDir, Now string } // Chat runs ONE turn of a conversation with the agent identified by agentKey, // resuming prior history keyed by sessionID. maxTurns is the per-turn agent step @@ -94,7 +95,7 @@ func (c *ChatAgent) Chat(ctx context.Context, agentKey, sessionID, message strin tools, def, cleanup := AugmentTools(ctx, agentKey, base) defer cleanup() - system, boundary := deferredSystem(chatSystem(agentKey, sessionWorkDir), def) + system, boundary := deferredSystem(chatSystem(agentKey, c.workDir, sessionWorkDir), def) opts := agentcore.Options{ Provider: c.prov, SystemPrompt: system, diff --git a/agent/goals.go b/agent/goals.go index 3c1341c..3674215 100644 --- a/agent/goals.go +++ b/agent/goals.go @@ -72,7 +72,7 @@ type GoalSpec struct { // // as + taskID, when non-nil/positive, wire the add_task_scope tool so the // decomposer can register the explicit asset scope it extracts from the goal. -func DecomposeGoals(ctx context.Context, c Config, goalText, desc string, as *db.AssetStore, taskID int64, emit func(db.Activity)) []GoalSpec { +func DecomposeGoals(ctx context.Context, c Config, dataDir, goalText, desc string, as *db.AssetStore, taskID int64, emit func(db.Activity)) []GoalSpec { prov, err := c.NewProvider() if err != nil { return nil @@ -102,7 +102,7 @@ func DecomposeGoals(ctx context.Context, c Config, goalText, desc string, as *db // Description rides in the user message (same channel as the goal), NOT via the // {{.EngagementDescription}} template var — else a prompt that references the var // would inject the description twice. System prompt stays pure static instructions. - sys := renderSystem("goals", goalsDefaultTmpl, GoalsVars{Now: nowStr()}) + sys := renderSystem("goals", goalsDefaultTmpl, GoalsVars{DataDir: dataDir, Now: nowStr()}) tools := []actool.CoreTool{submitTool} // Wire add_task_scope only when we have a real asset store + task to write to. // The scope-extraction tail is appended in lockstep so the prompt never asks for diff --git a/agent/mainagent.go b/agent/mainagent.go index 8b0b702..9269a25 100644 --- a/agent/mainagent.go +++ b/agent/mainagent.go @@ -54,8 +54,8 @@ const mainAgentDefaultTmpl = `你是一个授权渗透测试系统的"主 agent" 不要编造发现;只根据工具返回的真实数据回答。` -func mainAgentSystem(goal, workDir string) string { - body := renderSystem("mainagent", mainAgentDefaultTmpl, MainVars{Goal: goal, Now: nowStr()}) +func mainAgentSystem(goal, dataDir, workDir string) string { + body := renderSystem("mainagent", mainAgentDefaultTmpl, MainVars{Goal: goal, DataDir: dataDir, Now: nowStr()}) return body + artifactSpec(workDir) } @@ -76,7 +76,7 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t defer cleanup() // 本任务的工作目录 /,先建好。 mainDir := ensureRunDir(m.workDir, taskID, 0) - system, boundary := deferredSystem(mainAgentSystem(goal, mainDir), def) + system, boundary := deferredSystem(mainAgentSystem(goal, m.workDir, mainDir), def) opts := agentcore.Options{ Provider: m.prov, SystemPrompt: system, diff --git a/agent/planner.go b/agent/planner.go index 618ae99..ba297c6 100644 --- a/agent/planner.go +++ b/agent/planner.go @@ -228,8 +228,8 @@ const plannerDefaultTmpl = `你是一个授权渗透测试系统的"规划者" 宁可不生成,也不要重复或硬凑。简洁、克制、高效。` -func plannerSystem(goal, workDir string) string { - body := renderSystem("planner", plannerDefaultTmpl, PlannerVars{Goal: goal, Now: nowStr()}) +func plannerSystem(goal, dataDir, workDir string) string { + body := renderSystem("planner", plannerDefaultTmpl, PlannerVars{Goal: goal, DataDir: dataDir, Now: nowStr()}) return body + artifactSpec(workDir) } @@ -269,7 +269,7 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts } // 本任务的工作目录 /,先建好。 taskDir := ensureRunDir(p.workDir, taskID, 0) - system, boundary := deferredSystem(plannerSystem(goal, taskDir), def) + system, boundary := deferredSystem(plannerSystem(goal, p.workDir, taskDir), def) // planner 无自身墙钟预算;有 deadline 时把 MaxDuration 夹逼到剩余,让在跑的规划轮在 // 任务到点时进收尾(因超时→任务超时词,因步数→per-run 词)。 maxDur, clamped := clampMaxDuration(tc.DeadlineUnix, 0) diff --git a/agent/prompt.go b/agent/prompt.go index 2860cf7..00bb229 100644 --- a/agent/prompt.go +++ b/agent/prompt.go @@ -15,10 +15,10 @@ var PromptOverride func(agentKey string) (string, bool) // Prompt-variable structs — fields mirror each agent's catalog (docs §5a) so a // user template referencing a catalog variable renders; referencing anything else // fails template execution and falls back to the built-in default. -type PlannerVars struct{ Goal, Scope, AssetSummary, Now string } -type WorkerVars struct{ ProxyAddr, WorkerName, Now string } -type MainVars struct{ Goal, AssetSummary, FindingsSummary, Now string } -type GoalsVars struct{ EngagementDescription, Now string } +type PlannerVars struct{ Goal, Scope, AssetSummary, DataDir, Now string } +type WorkerVars struct{ ProxyAddr, WorkerName, DataDir, Now string } +type MainVars struct{ Goal, AssetSummary, FindingsSummary, DataDir, Now string } +type GoalsVars struct{ EngagementDescription, DataDir, Now string } // nowStr is the server-local wall-clock string exposed as the universal {{.Now}} // prompt variable. renderSystem runs on every agent turn/round, so this is fresh diff --git a/agent/prompt_now_test.go b/agent/prompt_now_test.go index 604b38e..4cd5f60 100644 --- a/agent/prompt_now_test.go +++ b/agent/prompt_now_test.go @@ -20,7 +20,7 @@ func TestChatNowVarRenders(t *testing.T) { return "", false } - out := chatSystem("tec_benchmark", "/tmp/x") + out := chatSystem("tec_benchmark", "/app/data", "/tmp/x") if !strings.Contains(out, "当前时间:") { t.Fatalf("custom prompt body missing, likely fell back to default: %q", out) } @@ -30,6 +30,23 @@ func TestChatNowVarRenders(t *testing.T) { } } +// TestChatDataDirVarRenders verifies the universal {{.DataDir}} runtime variable: +// a custom prompt referencing it renders the server data root (s.m.dir), rather +// than failing template execution and falling back to DefaultAssistantPrompt. +func TestChatDataDirVarRenders(t *testing.T) { + prev := PromptOverride + defer func() { PromptOverride = prev }() + + PromptOverride = func(key string) (string, bool) { + return "数据根目录:{{.DataDir}}", true + } + + out := chatSystem("tec_benchmark", "/app/data", "/tmp/x") + if !strings.Contains(out, "数据根目录:/app/data") { + t.Fatalf("{{.DataDir}} did not render the data root: %q", out) + } +} + // TestChatUnknownVarFallsBack verifies an out-of-catalog {{.X}} still degrades // safely to the default assistant prompt (never a half-rendered prompt). func TestChatUnknownVarFallsBack(t *testing.T) { @@ -40,7 +57,7 @@ func TestChatUnknownVarFallsBack(t *testing.T) { return "引用了不存在的变量:{{.Bogus}}", true } - out := chatSystem("whatever", "/tmp/x") + out := chatSystem("whatever", "/app/data", "/tmp/x") if strings.Contains(out, "引用了不存在的变量") { t.Fatalf("broken template should have fallen back, got custom body: %q", out) } diff --git a/agent/prompt_test.go b/agent/prompt_test.go index 9c9f921..66d7fe4 100644 --- a/agent/prompt_test.go +++ b/agent/prompt_test.go @@ -34,7 +34,7 @@ func TestRenderSystemOverrideAndFallback(t *testing.T) { // full plannerSystem path: DB body [A] is honored, then the code-owned tail // [C] (中间产物输出规约) is ALWAYS appended — editing the body can't drop it. PromptOverride = func(k string) (string, bool) { return "PLANNER {{.Goal}}", true } - got := plannerSystem("拿下X", "/data") + got := plannerSystem("拿下X", "/data", "/data") if !strings.HasPrefix(got, "PLANNER 拿下X") { t.Fatalf("plannerSystem body not honored: %q", got) } @@ -47,7 +47,7 @@ func TestRenderSystemOverrideAndFallback(t *testing.T) { PromptOverride = func(k string) (string, bool) { return "{{if .ProxyAddr}}走代理 {{.ProxyAddr}}{{else}}手动{{end}}", true } - withProxy := workerSystem("127.0.0.1:8080", "/data") + withProxy := workerSystem("127.0.0.1:8080", "/data", "/data") if !strings.HasPrefix(withProxy, "走代理 127.0.0.1:8080") { t.Fatalf("worker proxy branch body: %q", withProxy) } @@ -57,7 +57,7 @@ func TestRenderSystemOverrideAndFallback(t *testing.T) { if !strings.Contains(withProxy, "中间产物输出规约") { t.Fatalf("worker missing artifact tail: %q", withProxy) } - noProxy := workerSystem("", "/data") + noProxy := workerSystem("", "/data", "/data") if !strings.HasPrefix(noProxy, "手动") { t.Fatalf("worker no-proxy branch body: %q", noProxy) } diff --git a/agent/worker.go b/agent/worker.go index 39b37fc..4bd5f29 100644 --- a/agent/worker.go +++ b/agent/worker.go @@ -182,8 +182,8 @@ func ensureRunDir(base string, taskID, intentID int64) string { // cmdOutDir is the SDK large-tool-output spill dir under an agent's run dir. func cmdOutDir(dir string) string { return filepath.Join(dir, "cmd-output") } -func workerSystem(proxyAddr, runDir string) string { - body := renderSystem("worker", workerDefaultTmpl, WorkerVars{ProxyAddr: proxyAddr, Now: nowStr()}) +func workerSystem(proxyAddr, dataDir, runDir string) string { + body := renderSystem("worker", workerDefaultTmpl, WorkerVars{ProxyAddr: proxyAddr, DataDir: dataDir, Now: nowStr()}) return body + workerTrafficBlock(proxyAddr) + workerArtifactSpec(runDir) } @@ -263,7 +263,7 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db. // 压缩。本次意图的专属工作目录 //i,引擎侧先建好。 runDir := ensureRunDir(w.workDir, taskID, intent.ID) overview := renderWorkerGraphOverview(tsx.graphOverviewData()) - system, boundary := deferredSystem(workerSystem(w.proxyAddr, runDir), def) + system, boundary := deferredSystem(workerSystem(w.proxyAddr, w.workDir, runDir), def) // 任务级 deadline(经 ctx 注入)夹逼本 run 的墙钟预算 + 决定收尾词(见 taskclock.go)。 tc := taskClockFrom(ctx) maxDur, clamped := clampMaxDuration(tc.DeadlineUnix, w.runTimeout) diff --git a/db/config.go b/db/config.go index b33096b..df31ddf 100644 --- a/db/config.go +++ b/db/config.go @@ -142,7 +142,7 @@ type Agent struct { TriggerMaxParallel int `json:"trigger_max_parallel"` } -const agentCols = `id,key,name,COALESCE(description,''),role,builtin,enabled,COALESCE(max_turns,0),COALESCE(run_seconds,600),COALESCE(web_search,false),COALESCE(interactive_shell,false),COALESCE(wrapup_prompt,''),COALESCE(wrapup_max_turns,0),COALESCE(task_timeout_wrapup_prompt,''),COALESCE(task_timeout_wrapup_max_turns,0),COALESCE(trigger_run_mode,'serial'),COALESCE(trigger_merge_mode,'by_task'),COALESCE(trigger_max_parallel,5),llm_profile_id` +const agentCols = `id,key,name,COALESCE(description,''),role,builtin,enabled,COALESCE(max_turns,0),COALESCE(run_seconds,600),COALESCE(web_search,false),COALESCE(interactive_shell,false),COALESCE(wrapup_prompt,''),COALESCE(wrapup_max_turns,0),COALESCE(task_timeout_wrapup_prompt,''),COALESCE(task_timeout_wrapup_max_turns,0),COALESCE(trigger_run_mode,'serial'),COALESCE(trigger_merge_mode,'all'),COALESCE(trigger_max_parallel,5),llm_profile_id` func scanAgent(sc interface{ Scan(...any) error }) (*Agent, error) { var a Agent diff --git a/db/schema.sql b/db/schema.sql index 8a529c7..f4d7fba 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -300,7 +300,7 @@ CREATE TABLE IF NOT EXISTS agents ( task_timeout_wrapup_prompt TEXT NOT NULL DEFAULT '', task_timeout_wrapup_max_turns INTEGER NOT NULL DEFAULT 0, trigger_run_mode TEXT NOT NULL DEFAULT 'serial' CHECK (trigger_run_mode IN ('serial','parallel')), - trigger_merge_mode TEXT NOT NULL DEFAULT 'by_task' CHECK (trigger_merge_mode IN ('by_task','all','none')), + trigger_merge_mode TEXT NOT NULL DEFAULT 'all' CHECK (trigger_merge_mode IN ('by_task','all','none')), trigger_max_parallel INTEGER NOT NULL DEFAULT 5, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), @@ -308,7 +308,7 @@ CREATE TABLE IF NOT EXISTS agents ( ); -- 加列迁移(已发版,旧库升级补列;新库 CREATE 已含。迁移不带 CHECK:旧库存量安全 + 后端写入白名单兜底)。 ALTER TABLE agents ADD COLUMN IF NOT EXISTS trigger_run_mode TEXT NOT NULL DEFAULT 'serial'; -ALTER TABLE agents ADD COLUMN IF NOT EXISTS trigger_merge_mode TEXT NOT NULL DEFAULT 'by_task'; +ALTER TABLE agents ADD COLUMN IF NOT EXISTS trigger_merge_mode TEXT NOT NULL DEFAULT 'all'; ALTER TABLE agents ADD COLUMN IF NOT EXISTS trigger_max_parallel INTEGER NOT NULL DEFAULT 5; -- per-agent LLM 绑定(agent 级默认模型):列自初版即在上方 CREATE 中,此 ALTER 仅为极旧库兜底(幂等)。 ALTER TABLE agents ADD COLUMN IF NOT EXISTS llm_profile_id BIGINT REFERENCES llm_profiles(id) ON DELETE SET NULL; diff --git a/server/conversations.go b/server/conversations.go index aa49e16..ba692fc 100644 --- a/server/conversations.go +++ b/server/conversations.go @@ -363,9 +363,9 @@ type triggerBehavior struct { } // readTriggerBehavior loads an agent's策略, falling back to safe defaults -// (serial / by_task / 5) on any error or unknown enum value. +// (serial / all / 5) on any error or unknown enum value. func (s *Server) readTriggerBehavior(agentKey string) triggerBehavior { - b := triggerBehavior{runMode: "serial", mergeMode: "by_task", maxParallel: 5} + b := triggerBehavior{runMode: "serial", mergeMode: "all", maxParallel: 5} if s.m.pg == nil { return b } diff --git a/server/goals.go b/server/goals.go index d2776cc..aefb30e 100644 --- a/server/goals.go +++ b/server/goals.go @@ -65,7 +65,7 @@ func (s *Server) createGoals(ctx context.Context, t *Task, emit func(db.Activity as = s.m.Assets() } taskID, _ := strconv.ParseInt(t.ID, 10, 64) - for _, g := range agent.DecomposeGoals(ctx, cfg, t.Goal, t.Description, as, taskID, emit) { + for _, g := range agent.DecomposeGoals(ctx, cfg, s.m.dir, t.Goal, t.Description, as, taskID, emit) { if strings.TrimSpace(g.Text) != "" { specs = append(specs, goalSpec{Text: g.Text, VulnClass: g.VulnClass}) } diff --git a/server/orchestration.go b/server/orchestration.go index 019f9e5..2371fde 100644 --- a/server/orchestration.go +++ b/server/orchestration.go @@ -240,7 +240,6 @@ func (s *Server) toolSpawnTask() actool.CoreTool { "llm_profile_id": map[string]any{"type": "integer", "description": "可选:指定本子任务 planner/worker 用的 LLM 配置 id(见 list_llm_profiles);留空则继承父任务、再回退全局激活配置"}, "timeout_seconds": map[string]any{"type": "integer", "description": "可选:任务级超时(秒)。到点后触发优雅收尾并进入 timeout 终态;留空或 0 = 不限时"}, "plan_heartbeat_seconds": map[string]any{"type": "integer", "description": "可选:planner 心跳触发间隔(秒)。距上轮规划结束/任务开始满该值且期间无触发 → 触发一轮规划(兜底死锁 + 唤醒去监督飞行中的 worker)。留空或 0 = 默认 600(10min);"}, - "seed_first_intent": map[string]any{"type": "boolean", "description": "可选:默认 true。true=创建即把「描述+目标」作为一条种子意图下发,worker 免等首轮 planner 直接开跑(CTF 等常一个 work 解决的场景推荐);false=走标准的先规划再执行"}, }, "description", "goal"), func(_ context.Context, in json.RawMessage) (actool.Result, error) { var a struct { @@ -248,7 +247,6 @@ func (s *Server) toolSpawnTask() actool.CoreTool { LLMProfileID json.RawMessage `json:"llm_profile_id"` TimeoutSeconds int `json:"timeout_seconds"` PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"` - SeedFirstIntent *bool `json:"seed_first_intent"` } _ = json.Unmarshal(in, &a) if strings.TrimSpace(a.Description) == "" { @@ -283,8 +281,9 @@ func (s *Server) toolSpawnTask() actool.CoreTool { } } // 共享的建后流程,与 HTTP 建任务(server.go createTask)复用同一段 launchTask: - // seed + 种子意图 + 后台可见地做目标分解(第0轮/LLM步骤/逐条goal) + engine.Run。 - s.launchTask(t, a.Description+" "+a.Goal, a.SeedFirstIntent == nil || *a.SeedFirstIntent) + // seed + 后台可见地做目标分解(第0轮/LLM步骤/逐条goal) + engine.Run。 + // spawn_task 不暴露 seed_first_intent:工具创建的任务一律走标准先规划再执行(false)。 + s.launchTask(t, a.Description+" "+a.Goal, false) return actool.Text(fmt.Sprintf("task created: %s", t.ID)), nil }) } diff --git a/server/server.go b/server/server.go index 81a5190..439d249 100644 --- a/server/server.go +++ b/server/server.go @@ -1018,7 +1018,7 @@ type createTaskReq struct { LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务的 LLM 配置;省略/null=用激活配置 TimeoutSeconds int `json:"timeout_seconds"` // 任务级超时(秒);0/省略=不限时 PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"` // planner 心跳触发间隔(秒);0/省略=默认600(10min);下限=默认=600,低于自动抬到600 - SeedFirstIntent *bool `json:"seed_first_intent,omitempty"` // 创建时直接下发一条种子意图(内容=描述+目标),让 worker 免等首轮 planner 直接开跑;省略/null=默认开启。CTF 常一 work 解决,省掉开跑前的 planner 轮。 + SeedFirstIntent *bool `json:"seed_first_intent,omitempty"` // 创建时直接下发一条种子意图(内容=描述+目标),让 worker 免等首轮 planner 直接开跑;省略/null=默认关闭,走标准先规划再执行。显式传 true 才开(CTF 常一 work 解决时可省掉开跑前的 planner 轮)。 } func (s *Server) createTask(w http.ResponseWriter, r *http.Request) { @@ -1049,7 +1049,7 @@ func (s *Server) createTask(w http.ResponseWriter, r *http.Request) { log.Printf("[task] 新建任务 #%s «%s» 目标: %s", t.ID, req.Description, req.Goal) // 共享的建后流程(seed + 种子意图 + 后台目标分解 + engine.Run),与 spawn_task 复用同一段。 // launchTask 内部异步,不阻塞 UI —— 目标分解在后台可见地进行。 - s.launchTask(t, req.Description+" "+req.Goal, req.SeedFirstIntent == nil || *req.SeedFirstIntent) + s.launchTask(t, req.Description+" "+req.Goal, req.SeedFirstIntent != nil && *req.SeedFirstIntent) writeJSON(w, 201, t) } diff --git a/server/server_mgmt.go b/server/server_mgmt.go index 254a5eb..04ece0a 100644 --- a/server/server_mgmt.go +++ b/server/server_mgmt.go @@ -1749,6 +1749,7 @@ func (s *Server) pgListModels(w http.ResponseWriter, r *http.Request) { // {{.Now}} — e.g. subtract it from a fixed start stamp to reason about elapsed time. var globalPromptVars = []db.PromptVar{ {Name: "Now", Description: "服务端当前时间(每次运行实时刷新;可与固定起始时间相减判断已用时长)", Example: "2026-08-11 14:30:00 CST", Source: "runtime"}, + {Name: "DataDir", Description: "服务端数据根目录(所有任务/会话产物的根;各 agent 实际写盘在其下的子目录,如 /)", Example: "/app/data", Source: "runtime"}, } // withGlobalVars appends the universal runtime vars onto an agent's own catalog, diff --git a/web/src/app/(main)/function/tasks/page.tsx b/web/src/app/(main)/function/tasks/page.tsx index a5cd170..6a2ddc8 100644 --- a/web/src/app/(main)/function/tasks/page.tsx +++ b/web/src/app/(main)/function/tasks/page.tsx @@ -112,7 +112,7 @@ export default function TasksPage() { const [llmProfile, setLlmProfile] = React.useState(ACTIVE_PROFILE); // sentinel = active const [timeoutMin, setTimeoutMin] = React.useState(""); // 任务级超时(分钟);空/0 = 不限时 const [heartbeatMin, setHeartbeatMin] = React.useState("10"); // planner 心跳(分钟);默认10,下限10(与后端一致) - const [seedFirstIntent, setSeedFirstIntent] = React.useState(true); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑 + const [seedFirstIntent, setSeedFirstIntent] = React.useState(false); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑;默认关闭,走标准先规划再执行 const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(""); const [statusFilter, setStatusFilter] = React.useState("all"); @@ -182,7 +182,7 @@ export default function TasksPage() { setLlmProfile(ACTIVE_PROFILE); setTimeoutMin(""); setHeartbeatMin("10"); - setSeedFirstIntent(true); + setSeedFirstIntent(false); setOpen(false); load(); } catch (e) { diff --git a/web/src/components/agent-editor.tsx b/web/src/components/agent-editor.tsx index 54126b6..a678fda 100644 --- a/web/src/components/agent-editor.tsx +++ b/web/src/components/agent-editor.tsx @@ -734,11 +734,11 @@ function AgentTriggersTab({ agentKey, agent }: { agentKey: string; agent?: Agent const [tools, setTools] = React.useState([]); // 触发后处理策略(每 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 [mergeMode, setMergeMode] = React.useState<"by_task" | "all" | "none">(agent?.trigger_merge_mode ?? "all"); 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"); + setMergeMode(agent?.trigger_merge_mode ?? "all"); setMaxParallel(String(agent?.trigger_max_parallel ?? 5)); }, [agent?.trigger_run_mode, agent?.trigger_merge_mode, agent?.trigger_max_parallel]); diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 48567d2..664bcb1 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -115,7 +115,7 @@ export const api = { goal, llm_profile_id: llmProfileId ?? null, timeout_seconds: timeoutSeconds ?? 0, - seed_first_intent: seedFirstIntent ?? true, + seed_first_intent: seedFirstIntent ?? false, plan_heartbeat_seconds: planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min) }), deleteTask: (id: string) => del<{ deleted: number }>(`/tasks/${id}`),