diff --git a/.gitignore b/.gitignore index f7b1733..8d9da17 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ Thumbs.db # 调度器运行/模拟产生的事件日志(测试产物,不提交) *-events.csv + +# bench 调试目录(本地调试产物,不提交) +/bench/debug/ diff --git a/agent/planner.go b/agent/planner.go index 4dcde86..618ae99 100644 --- a/agent/planner.go +++ b/agent/planner.go @@ -256,19 +256,20 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts base := append(tsx.PlannerTools(), actool.DefaultTools()...) tools, def, cleanup := AugmentTools(ctx, "planner", base) defer cleanup() - // 关键态势(刚完成的意图 + 预取的完整图)放进 system prompt,而不是 user 输入: - // system 段永不被 compaction 压缩,长回合里也保证这份态势始终在场;轮内固定,还能吃到 - // system 缓存。它落在指令之后、deferred 工具块之前(deferredSystem 保证块在最后)。 + // 关键态势(刚完成的意图 + 预取的完整图)改放【本轮 user 输入】(见下方 input),system + // 只留静态规划正文。move-out 让 system 每轮稳定、更利于缓存;代价是若单轮变长,态势可能 + // 被 compaction 压缩(planner 单轮通常短,风险低)。situational 会拼进下方 input。 situational := renderTriggers(ts, triggers) + renderGraphOverview(tsx.graphOverviewData()) // 任务级 deadline / 终局模式(经 ctx 注入,见 taskclock.go)。终局那一轮把任务超时 - // planner 收尾词作为【本轮操作指令】直接注入 system,让它只做最后目标判定、不产新意图。 + // planner 收尾词作为【本轮操作指令】拼进本轮 user 输入(随 situational),让它只做最后 + // 目标判定、不产新意图。 tc := taskClockFrom(ctx) if tc.Final { situational += "\n\n【任务终局收尾(本轮特殊指令,覆盖上面的常规规划流程)】:" + resolveTaskTimeoutWrapup("planner") } // 本任务的工作目录 /,先建好。 taskDir := ensureRunDir(p.workDir, taskID, 0) - system, boundary := deferredSystem(plannerSystem(goal, taskDir)+situational, def) + system, boundary := deferredSystem(plannerSystem(goal, taskDir), def) // planner 无自身墙钟预算;有 deadline 时把 MaxDuration 夹逼到剩余,让在跑的规划轮在 // 任务到点时进收尾(因超时→任务超时词,因步数→per-run 词)。 maxDur, clamped := clampMaxDuration(tc.DeadlineUnix, 0) @@ -311,15 +312,15 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts opts.Transcript = p.tx opts.SessionID = fmt.Sprintf("exp%d-planner", ts.ID()) } - // 态势(刚完成的意图 + 完整图)已在上面拼进 system prompt(抗压缩、轮内常驻)。这里 - // 的 user 输入只留指令 + 跨唤醒待办(todo 是模型自己的规划便签,可再生,放 user 即可)。 - // 开场白按「本轮有无具体变动」分两种:有变动 → 指向上方【实际变动】块;无变动 + // 态势(刚完成的意图 + 完整图)现在拼进本轮 user 输入(见下方 input)。user 里还有 + // 指令 + 跨唤醒待办(todo 是模型自己的规划便签,可再生,放 user 即可)。 + // 开场白按「本轮有无具体变动」分两种:有变动 → 指向下方【实际变动】块;无变动 // (心跳定时巡检 / hint / 恢复等) → 别谎称"图发生了变化",转而提示顺带复查在跑意图。 - lead := "刚有具体变动(见上方 system 的【本次触发本轮的实际变动】),据此规划下一步:" + lead := "刚有具体变动(见下面的【本次触发本轮的实际变动】),据此规划下一步:" if len(triggers) == 0 { lead = "本轮是**定时巡检(心跳到点)/无具体变动信号**的唤醒——图不一定有新变动。顺带复查在跑意图:长时间无进展或跑偏的用 steer_work 纠偏、方向整个错的用 kill_work 止损;再判定目标、决定是否补方向:" } - input := lead + "读取上方 system 里的态势,判定目标。**本轮若无未被覆盖的新方向,直接结束即可(生成 0 个意图是正常且常见的,尤其刚派完意图在等 worker 产出时)。绝不要为了“结束本轮”去调 goal_met——goal_met 会【立即结束整个任务】,只在你确认目标已【真正达成】(已拿到目标成果/已确认目标漏洞)时才调;未达成就用 prove_goal 逐个标记、或什么都不调直接结束。**" + + input := lead + situational + "\n\n据上面的态势,判定目标。**本轮若无未被覆盖的新方向,直接结束即可(生成 0 个意图是正常且常见的,尤其刚派完意图在等 worker 产出时)。绝不要为了“结束本轮”去调 goal_met——goal_met 会【立即结束整个任务】,只在你确认目标已【真正达成】(已拿到目标成果/已确认目标漏洞)时才调;未达成就用 prove_goal 逐个标记、或什么都不调直接结束。**" + renderPlannerTodos(opts.Todos.List()) // 有 deadline 夹逼时加硬 ctx 兜底(软预算 + grace),防单轮卡死绕过轮边界软超时。 runCtx := ctx diff --git a/agent/worker.go b/agent/worker.go index f71eede..39b37fc 100644 --- a/agent/worker.go +++ b/agent/worker.go @@ -140,7 +140,6 @@ const workerDefaultTmpl = `你是一个授权渗透测试系统的"执行者"(wo - record_fact:把探索【事实/结论】写入探索图并连到意图(传 intent_id)。正向/否定结论、观察、判断用它;**一次探索的多个观察汇总成一条事实**(summary 总结一句话 + detail 放细节),不要一个属性一条。真有多条不同结论才用 facts 数组。**只写你真实看到的**:给 evidence(一行关键证据:命令+关键输出,简洁,别粘大段——细节在 detail)、标 confidence(observed 直接看到 / inferred 推断);**否定结论**(不可注入/端口关闭等)尤其要给证据、证据弱就标 inferred,别让错的否定误导规划者放弃方向。 - report_finding:确认漏洞 → 记录(含 PoC,传 intent_id=你领到的意图id)。**只有你在本次运行里真实触发过该漏洞、拿到了可复现的证据(请求/响应或命令输出)才用它。** 严禁把下列当作已确认漏洞上报:仅凭版本号/指纹匹配到某 CVE、仅凭"参数看起来可注入"、仅凭外部漏洞库/更新日志/代码 diff 推断。**不要用查 CVE 库或"对比补丁版本"替代实际触发。** 触发不了但确有嫌疑,就用 record_fact 记一条 confidence=inferred 的事实(描述嫌疑点+为何未能触发),交给规划者派后续意图,别硬记成 finding。 - list_assets(查询资产,非探索节点) / asset_neighbors / list_facts(探索事实) / list_findings(漏洞) / node_detail(探索节点 id,非资产 id):按需查上下文。 -- 【必要时才会使用,大多数上下文都在本次会话中】search_all_worker_traces / list_worker_traces / get_worker_trace:**跨 work 复用信息**(别的 work 执行过程里出现过、却没写进 fact 的东西)。你看不到探索图,但可以:search_all_worker_traces(q) 按关键字搜全部 work 的过程(返回带 intent_id);list_worker_traces 看有哪些 work 跑过;get_worker_trace(intent_id) 看某 work 的步骤摘要、get_worker_trace(intent_id, step_ids=[…]) 取那几步完整内容(一次≤5个)。**仅用于复用他人观察、避免重复劳动——不改变你的任务边界(仍只做你这条意图)。** 只在授权范围内操作。完成本意图后用一句话总结你做了什么、写回了哪些事实。务实、克制、聚焦这一条意图。` @@ -188,9 +187,10 @@ func workerSystem(proxyAddr, runDir string) string { return body + workerTrafficBlock(proxyAddr) + workerArtifactSpec(runDir) } -// renderIntentTask formats the claimed intent for the worker's SYSTEM prompt: the -// intent is the worker's whole job, so — like the planner's situational block — it -// belongs in system, where compaction can never drop it during a long run. +// renderIntentTask formats the claimed intent for the worker's launch USER message: +// the intent is the worker's whole job. It used to live in the system prompt; it now +// rides in the first user turn (together with the situational overview) so the system +// prompt stays static/role-only — same move as the planner's situational block. // intentAssetIDs pulls the intent's target asset ids out of its payload // (planner's add_intent stores them as a numeric asset_ids array). nil on absence // or malformed payload. @@ -212,7 +212,7 @@ func renderIntentTask(intent *db.Node) string { } // renderWorkerGraphOverview folds the global situational snapshot into the worker's -// SYSTEM prompt for AWARENESS ONLY. The framing is deliberately strong: the overview +// launch USER message for AWARENESS ONLY. The framing is deliberately strong: the overview // must NOT widen the worker's job — it still does only its assigned intent. Its sole // purpose is letting the worker read context (existing facts/assets/hints) // so it avoids redundant work and doesn't re-derive what others already found. @@ -226,8 +226,7 @@ func renderWorkerGraphOverview(data map[string]any) string { return "" // fall back silently: the worker just won't have the global context } return "\n\n【全局探索态势(仅供你了解大局,不是你的任务清单)】:\n" + - "下面是整个任务当前的探索概况。给你的**唯一目的**是让你了解全局动态、复用已有结论、避免重复别人已做过的事。\n" + - "**开场别做前置普查**:这份概况已够你起步,直接开打领到的意图;list_facts / list_findings / list_worker_traces 只在执行中真缺某条细节时才按需查。\n" + + "下面是整个任务当前的探索概况。给你的**唯一目的**是让你了解全局动态。\n" + "**它绝不扩大你的职责边界**:你仍然只做上面领到的那一条意图。看到这里有别的 open 意图 / 未覆盖的点 / 其它可打方向,也**绝不要自己去动手**——那些是别的 worker 的事,由规划者调度。你若发现相关新线索,最多写进 fact 让规划者知道,不要自己追。\n" + string(b) } @@ -258,13 +257,13 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db. tools, def, cleanup := AugmentTools(ctx, "worker", base) defer cleanup() - // 意图是 worker 的全部任务,放进 system prompt(抗 compaction、整轮常驻),而不是 - // user 输入——与 planner 把关键态势放 system 一致。落在指令之后、deferred 块之前。 - // 全局态势(graphOverviewData)也带上,但仅供了解大局;意图放在最后、最醒目的位置。 - // 本次意图的专属工作目录 //i,引擎侧先建好。 + // 意图 + 全局态势改放【启动 user 消息】(见下方 input),system 只留静态角色正文 + // (段[A]/[B]/[C] + deferred 块)。与 planner 一致:把易变的运行期数据移出 system, + // system 每 session 稳定、更利于缓存;代价是长 run 里这条 user 消息可能被 compaction + // 压缩。本次意图的专属工作目录 //i,引擎侧先建好。 runDir := ensureRunDir(w.workDir, taskID, intent.ID) overview := renderWorkerGraphOverview(tsx.graphOverviewData()) - system, boundary := deferredSystem(workerSystem(w.proxyAddr, runDir)+overview+renderIntentTask(intent), def) + system, boundary := deferredSystem(workerSystem(w.proxyAddr, runDir), def) // 任务级 deadline(经 ctx 注入)夹逼本 run 的墙钟预算 + 决定收尾词(见 taskclock.go)。 tc := taskClockFrom(ctx) maxDur, clamped := clampMaxDuration(tc.DeadlineUnix, w.runTimeout) @@ -326,9 +325,11 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db. emit(r) } } - // 意图已在 system 里。user 输入只放启动指令 + 意图锚定资产的原始数据(供 worker 直接 - // 用,省去开场再查一次 list_assets)。原始 JSON 直接附上,不做提取/格式化。 - input := "开始执行 system 提示里的这条意图:只做它、只产生事实、做完即停。" + // 意图 + 全局态势 + 启动指令 + 意图锚定资产的原始数据都放这条启动 user 消息里。 + // 意图放最前、最醒目;overview 仅供了解大局。资产原始 JSON 直接附上,不做提取/格式化, + // 省去开场再查一次 list_assets。注意:这些运行期数据现在活在 user 消息里,长 run 中 + // 有被 compaction 压缩的风险(意图是 worker 全部职责,若被压掉需另行 pin,待评估)。 + input := renderIntentTask(intent) + overview + "\n\n开始执行上面这条意图:只做它、只产生事实、做完即停。" if as != nil { if ids := intentAssetIDs(intent); len(ids) > 0 { if assets, err := as.GetByIDs(ids); err == nil && len(assets) > 0 { diff --git a/bench/prompts/challenge_review.md b/bench/prompts/challenge_review.md index f21dcb3..26f2fb2 100644 --- a/bench/prompts/challenge_review.md +++ b/bench/prompts/challenge_review.md @@ -1,4 +1,6 @@ -你是自动化 CTF 跑分系统里的「题目经验复用」agent。**触发条件:任务创建时触发**——每当有新题目任务被创建,你被唤醒一次。你的**唯一职责**:判断这道题以前是否做过,若做过,就把上次积累的可复用经验通过 hint 注入到这个新任务,帮它少走弯路、避免重复劳动。 +你是自动化 CTF 跑分系统里的「题目经验复用」agent。 + +**触发条件**:任务创建时触发——每当有新题目任务被创建,你被唤醒一次。你的**唯一职责**:判断这道题以前是否做过,若做过,就把上次积累的可复用经验注入到这个新任务,帮它少走弯路、避免重复劳动。若没做过,直接退出。 ## 核心概念 - 题目的唯一标识是 `unique_code`(题目 id);**当前新任务的信息里包含本题的 unique_code**。 @@ -9,15 +11,48 @@ 1. 从当前任务信息里取出本题的 `unique_code` 和当前任务 id。 2. 用 `list_tasks` 列出全部任务,筛出 **unique_code 与本题完全相同** 的历史任务(排除当前这个)。 3. **若没有其它同 unique_code 的历史任务(即只有当前这一个)→ 说明是首次做,立即结束本次对话,绝不调用任何多余工具!!** -4. 若存在历史任务:用 `get_task_graph` / `list_task_findings` / `list_task_worker_traces` / `get_task_worker_trace` / `search_task_worker_traces` 查看它们的**探索结论、确认的发现、以及 worker 的执行过程**,提炼**可复用的事实/经验**,例如: - - 已确认的技术栈/版本/指纹、有效的注入点/参数/端点、可用凭据或绕过手法、解题的关键路径; - - 已排除的死路(某参数不可注入、某端口关闭、某方向无果等),让新任务不再浪费时间重试。 - - 思考是从之前的运行结果中是否有一些其他路可以走,只要能达到目标即可,可以根据已经获取到的事实、结论拓展各种手法。 -5. 用 `add_task_hint` 把这些经验注入**当前这个新任务**,供规划者据此更快推进。 +4. 若存在历史任务:用 `get_task_graph` / `list_task_findings` / `list_task_worker_traces` / `get_task_worker_trace` / `search_task_worker_traces` 查看它们的**探索结论、确认的发现、以及 worker 的执行过程**,提炼**可复用的事实/经验**。 -## 硬性规则 -- **首做即退**:同 unique_code 只有当前一个任务时,立刻退出,**绝不调用多余工具**(省 token、省时间)。 -- **unique_code 必须完全一致**才算同题;不要把别的题的经验混进来。 -- **忽略目标地址**:每次重试的 base url / container_addr 会变——注入 hint 时**只写与地址无关的经验**(打法、结论、死路),**不要把上次的具体 URL/IP 当成本次目标**。 -- hint 要**具体、可操作**:直接写「上次在 X 类接口用 Y 手法拿到 flag」「上次确认 Z 方向不可行,跳过」,而不是空泛的「仔细测试」。 -- **只做经验复用这一件事**:不要自己去解题、不要创建或暂停任务、不要提交 flag。 +## 经验抽取原则 + +### 1. 区分「事实」与「推测」 + +| 类型 | 定义 | 示例 | 标注 | +|---|---|---|---| +| `confirmed` | 有明确的响应/数据/证据支撑 | "gopher 协议返回「不支持的协议」" | confidence: high | +| `inferred` | 基于间接证据或逻辑推断 | ".4:80 曾返回 200,可能是 flag 容器" | confidence: medium | +| `negative` | 某方向尝试后无结果 | "路径枚举未发现新端点(仅枚举路径,未测参数名)" | confidence: low-medium,必须标注局限性 | + +### 2. 限制「已穷尽」的使用 + +**「已穷尽」是最高等级的结论,必须满足以下全部条件才能使用:** +- 至少使用了 **2 种不同方法** 验证(如:路径枚举 + 参数名穷举 + 方法变体) +- 每种方法都达到了合理的覆盖深度 +- 验证过程的证据可追溯 + +**若未达到上述标准,必须写「局限性」而非「已穷尽」:** + +| ❌ 错误写法 | ✅ 正确写法 | +|---|---| +| "internal-api 已穷尽,无更多端点" | "internal-api 路径枚举未发现新端点(仅枚举路径,未测参数名变体)" | +| ".4 是死路,不可达" | ".4 当前返回 No route to host,建议作为低优先级方向" | +| "MySQL 无法利用" | "MySQL 3306 从 SSRF 侧不可达(仅容器内 localhost 监听),无需重试" | + +### 3. 经验要「可操作」 + +每个 hint 必须包含: +- **是什么**:结论/发现 +- **怎么验证的**:关键证据 +- **有什么局限**:方法盲区 +- **建议怎么做**:给后续 Agent 的具体操作指引 + +## Hint 注入格式 + +每个 hint 必须按以下**结构化格式**写入: + +类型: confirmed | inferred | negative +置信度: 0.0-1.0 +摘要: <一句话总结> +详情: <详细说明,含证据、方法、局限性> +建议: <给后续 Agent 的具体建议> +局限性: <这个结论的局限性(如果有)> \ No newline at end of file diff --git a/bench/prompts/worker.md b/bench/prompts/worker.md index ff45c45..af6665c 100644 --- a/bench/prompts/worker.md +++ b/bench/prompts/worker.md @@ -1,7 +1,7 @@ 你是一个授权渗透测试系统的"执行者"(work agent)。你领到【一条意图】(一句话探索方向),唯一职责是:**完成这一条意图、把发现的事实写回知识图谱,然后停止返回。** 当前是在CTF场景下。 铁律(务必遵守): -1. **只做这一条意图**。意图边界就是你的红线。指纹意图就只做指纹识别(curl -I / whatweb / 看响应头与首页),不要顺手去枚举端点、爆破目录、扒 JS 找 API、测漏洞——那些是【别的意图】的事,由规划者去派别的 worker。 +1. **只做这一条意图**。意图边界就是你的红线。指纹意图就只做指纹识别(curl -I/ 看响应头与首页),不要顺手去枚举端点、爆破目录、扒 JS 找 API、测漏洞——那些是【别的意图】的事,由规划者去派别的 worker。 2. **你不负责"探索方向"**。发现了值得继续追的新线索,不要自己接着打;只要把它写回图,规划者会读到、自己生成新意图。生成探索方向是规划者的职责,不是你的。 3. **完成意图就立即返回**。意图达成了就停,别因为"任务总目标还没达成"就自己继续往下探——那是规划者下一轮的事。 4. **边发现边写回,并且写对地方**。每得出一个结果就立刻写回(别攒到最后,否则步数耗尽全丢)。结果只算写进图里的,活在你脑子/文字里的不算。**两张图分清楚**: @@ -14,16 +14,17 @@ - record_fact:把探索【事实/结论】写入探索图并连到意图(传 intent_id)。正向/否定结论、观察、判断用它;**一次探索的多个观察汇总成一条事实**(summary 总结一句话 + detail 放细节),不要一个属性一条。真有多条不同结论才用 facts 数组。**只写你真实看到的**:给 evidence(一行关键证据:命令+关键输出,简洁,别粘大段——细节在 detail)、标 confidence(observed 直接看到 / inferred 推断);**否定结论**(不可注入/端口关闭等)尤其要给证据、证据弱就标 inferred,别让错的否定误导规划者放弃方向。 - report_finding:确认漏洞 → 记录(含 PoC,传 intent_id=你领到的意图id)。 - list_assets(查询资产,非探索节点) / asset_neighbors / list_facts(探索事实) / list_findings(漏洞) / node_detail(探索节点 id,非资产 id):按需查上下文。 -- search_all_worker_traces / list_worker_traces / get_worker_trace:**跨 work 复用信息**(别的 work 执行过程里出现过、却没写进 fact 的东西)。你看不到探索图,但可以:search_all_worker_traces(q) 按关键字搜全部 work 的过程(返回带 intent_id);list_worker_traces 看有哪些 work 跑过;get_worker_trace(intent_id) 看某 work 的步骤摘要、get_worker_trace(intent_id, step_ids=[…]) 取那几步完整内容(一次≤5个)。**仅用于复用他人观察、避免重复劳动——不改变你的任务边界(仍只做你这条意图)。** - sleep:循环等待后台命令产出时用(之后再用 TaskOutput 查看部分输出)。 - 交互式PTY shell(高优先级):遇到 ssh/telnet、反弹shell 等任何需要"输入-响应"交互的服务,**高优先级**选择 shell_open 开交互式会话,使用对应的命令进行测试比如ssh/telnet。 + 只在授权范围内操作。完成本意图后用一句话总结你做了什么、写回了哪些事实。务实、克制、聚焦这一条意图。 +【关于目录扫描】:除了使用固定字典fuzz之外,还可以通过各种组合进行生成测试。 请勿探索该任务目标以外的资产,例如同C段ip等,因为是CTF靶场,所以不同题目只是C段不同,你只需要专注本次任务,除了一种情况是多阶段渗透测试,会出现多个地址,但是也是通过本任务找到的关联资产,不属于其他资产。 -对于已知产品,优先使用nuclei运行一遍。 -对于已知产品,离线的POC知识库也可以使用。 +【深度挖掘】当出现一些方向时,你需要注意进行深度挖掘,所有异常信息都可能是线索。只有将某个方向的所有手法完全穷尽才能任务该方向已经探索完毕。 +【重要】对于已知产品,优先使用nuclei运行一遍。;对于已知产品,离线的POC知识库也可以使用,检索到相关POC可以先进行打一遍。 flag如果是在文件中的话,通常在/challenge/flag.txt 或者/challenge/目录下其他文件位置 diff --git a/bench/scheduler.py b/bench/scheduler.py index a554405..8b95812 100644 --- a/bench/scheduler.py +++ b/bench/scheduler.py @@ -40,7 +40,10 @@ TOTAL_BUDGET = 1080.0 # 槽·分钟总预算 = 3 槽 × 360min(硬天花 SLOTS = 3 # 平台硬上限:同时最多 3 个活跃容器 DEADLINE_MIN = 360.0 # 6h 硬停 SOFT_DEADLINE = 350.0 # 软截止:此后不新起长窗,留 10min 优雅收尾 -POLL_SEC = 20 # 轮询周期(真实模式) +POLL_SEC = 20 # 基础轮询周期(真实模式,空闲时用) +FAST_POLL_SEC = float(os.environ.get("FAST_POLL_SEC", "3")) # 有活跃槽解题时的快轮询周期: + # 提交 flag/通关后平台是同步生效的,快轮询让"关旧题→补新题"几乎立刻发生, + # 把换题延迟从最多一个 POLL_SEC 压到 FAST_POLL_SEC。若平台限流可调大。 # —— 易题阶段 —— EASY_WALL_CAP = 95.0 # 墙钟到此强制收口、剩余易题 park、全槽转硬 @@ -408,7 +411,8 @@ class Scheduler: if self._all_done(): break - self.clock.sleep(POLL_SEC) + # 有槽在解题 → 快轮询,让提交/通关后立刻换题;全空闲(等靶场/无题)才回落基础周期省 QPS。 + self.clock.sleep(FAST_POLL_SEC if any(s.busy for s in self.slots) else POLL_SEC) self._shutdown() @@ -599,16 +603,19 @@ class Scheduler: else: st, ecode, addrs = self.b.start(code) if st == 409 and ecode == "invalid_state": - return # 达 3 活跃上限(或任务将结束):下一轮重试,challenges() 会捕获真结束 + self._requeue(code) # 达 3 活跃上限(或任务将结束):退回队列下一轮重试,challenges() 会捕获真结束 + return # ★ code 已被 _pick_next pop 出,不 requeue 会永久蒸发 if st == 503 or ecode == "resource_unavailable": - return # 靶场资源未就绪:稍后重试 + self._requeue(code) # 靶场资源未就绪:退回队列稍后重试 + return if st == 404 and ecode == "challenge_not_found": self.log.emit("放弃", code, "challenge_not_found,跳过") self.given_up.add(code) return if st == 404 and ecode == "task_not_found": raise BenchEnded("task_not_found") - if not addrs: # pending/无地址:下一轮重试 + if not addrs: # pending/无地址:退回队列下一轮重试 + self._requeue(code) return self.log.emit("启动靶场", code, f"container_addr: {', '.join(addrs)}") # 建任务前删资产:清掉复用 IP 上的旧题残留资产(host=地址去掉端口),避免跨题污染。 @@ -714,7 +721,10 @@ class Scheduler: self.easyQ.insert(0, code) def _vpn_guard(self, now): - """VPN 断开检测。返回 True=当前冻结、本轮跳过解题。恢复时把停摆时长从各槽预算补偿掉。""" + """VPN 断开检测。返回 True=当前冻结、本轮跳过平台处置(拉题/起停/对账都需 VPN)。 + 任务【不暂停】——大概率只是网络抖动,pause 会 cancel 掉在飞的 LLM/worker 执行、白烧 + 已花 token 且丢当前轮进度,得不偿失;抖动几秒对解题无害,等自然重连即可。 + 恢复时把停摆时长从各槽预算/停滞看门狗计时里补偿掉,避免停摆期误判并触发干净重启。""" ok = self.sim.vpn_ok() if self.simulate else self.b.vpn_ok() if ok: if self.frozen: @@ -725,19 +735,14 @@ class Scheduler: s.started += outage # 停摆不计入该题已跑时长 if s.last_progress is not None: s.last_progress += outage # 也不让停摆触发停滞看门狗 - if not self.simulate and s.task_id: - self.a.control(s.task_id, "resume") - self.log.emit("VPN恢复", info=f"停摆 {outage:.0f}min:各槽预算已补偿、任务恢复(墙钟仍走)") + self.log.emit("VPN恢复", info=f"停摆 {outage:.0f}min:预算已补偿、任务全程未停(墙钟仍走)") self.frozen = False return False - # VPN 断 + # VPN 断:任务不动(照跑),只跳过本轮需 VPN 的平台处置,等自然重连。 if not self.frozen: self.frozen = True self.freeze_start = now - for s in self.slots: - if s.busy and not self.simulate and s.task_id: - self.a.control(s.task_id, "pause") - self.log.emit("VPN断开", info="冻结各槽预算 + 暂停任务,等待恢复") + self.log.emit("VPN断开", info="跳过本轮平台处置、冻结各槽预算;任务不暂停,等待重连") return True def _clean_restart(self, s, now): diff --git a/go.mod b/go.mod index 00afaca..d56340a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Autumn-27/artex go 1.26.3 require ( - github.com/Autumn-27/norma v0.2.8 + github.com/Autumn-27/norma v0.2.9 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/jackc/pgx/v5 v5.10.0 github.com/lqqyt2423/go-mitmproxy v1.9.2 diff --git a/go.sum b/go.sum index 58e9cad..835017c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Autumn-27/norma v0.2.8 h1:hfNwGUC4Yt2TEwxIlPvhi0g3Xy4ILajNI2HGPbWNwGA= -github.com/Autumn-27/norma v0.2.8/go.mod h1:BudWrf7iHl96FgHqFr/HwN3mKYmGjJDt+xL2ZHXfFZw= +github.com/Autumn-27/norma v0.2.9 h1:VTt8Oe7XoiV85Dc+QOPu+zQ7IykQRm2aDTEbLEzbBe4= +github.com/Autumn-27/norma v0.2.9/go.mod h1:BudWrf7iHl96FgHqFr/HwN3mKYmGjJDt+xL2ZHXfFZw= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= diff --git a/web/src/app/(main)/function/tasks/page.tsx b/web/src/app/(main)/function/tasks/page.tsx index cd5b8bc..a5cd170 100644 --- a/web/src/app/(main)/function/tasks/page.tsx +++ b/web/src/app/(main)/function/tasks/page.tsx @@ -111,7 +111,7 @@ export default function TasksPage() { const [profiles, setProfiles] = React.useState([]); const [llmProfile, setLlmProfile] = React.useState(ACTIVE_PROFILE); // sentinel = active const [timeoutMin, setTimeoutMin] = React.useState(""); // 任务级超时(分钟);空/0 = 不限时 - const [heartbeatMin, setHeartbeatMin] = React.useState("5"); // planner 心跳(分钟);默认5,下限5(与后端一致) + const [heartbeatMin, setHeartbeatMin] = React.useState("10"); // planner 心跳(分钟);默认10,下限10(与后端一致) const [seedFirstIntent, setSeedFirstIntent] = React.useState(true); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑 const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(""); @@ -174,14 +174,14 @@ export default function TasksPage() { try { const pid = llmProfile === ACTIVE_PROFILE ? undefined : Number(llmProfile); const timeoutSec = Math.max(0, Math.floor(Number(timeoutMin) || 0)) * 60; - const heartbeatSec = Math.max(5, Math.floor(Number(heartbeatMin) || 5)) * 60; // 下限 5min,与后端归一一致 + const heartbeatSec = Math.max(10, Math.floor(Number(heartbeatMin) || 10)) * 60; // 下限 10min,与后端归一一致 await api.createTask(description.trim(), goal.trim(), pid, timeoutSec, seedFirstIntent, heartbeatSec); toast.success("任务已创建"); setDescription(""); setGoal(""); setLlmProfile(ACTIVE_PROFILE); setTimeoutMin(""); - setHeartbeatMin("5"); + setHeartbeatMin("10"); setSeedFirstIntent(true); setOpen(false); load(); @@ -311,14 +311,14 @@ export default function TasksPage() { setHeartbeatMin(e.target.value)} />

- 距上轮规划结束/任务开始满该时长且期间无触发,自动触发一轮规划(兜底卡死 + 唤醒去监督在跑的 worker)。下限 5 分钟。 + 距上轮规划结束/任务开始满该时长且期间无触发,自动触发一轮规划(兜底卡死 + 唤醒去监督在跑的 worker)。下限 10 分钟。

diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index bcc8d61..48567d2 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -116,7 +116,7 @@ export const api = { llm_profile_id: llmProfileId ?? null, timeout_seconds: timeoutSeconds ?? 0, seed_first_intent: seedFirstIntent ?? true, - plan_heartbeat_seconds: planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 300(5min) + plan_heartbeat_seconds: planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min) }), deleteTask: (id: string) => del<{ deleted: number }>(`/tasks/${id}`), controlTask: (id: string, action: "pause" | "resume") => post<{ id: string; paused: boolean }>(`/tasks/${id}/control`, { action }),