181 lines
5.3 KiB
Go
181 lines
5.3 KiB
Go
package agent
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/docker/docker/api/types/container"
|
||
"github.com/docker/docker/api/types/filters"
|
||
"github.com/docker/docker/client"
|
||
)
|
||
|
||
// CoopTask 记录一次后台运行的 pi 协作任务。
|
||
// 任务由 run_coop 工具异步启动,完成后通过 CoopManager.Complete 通知主 agent。
|
||
type CoopTask struct {
|
||
ID string `json:"id"`
|
||
SessionID string `json:"session_id"`
|
||
ContainerID string `json:"container_id,omitempty"`
|
||
// ProcessID 是本地子进程模式的 worker 进程 PID(docker 模式为 0)。
|
||
ProcessID int `json:"process_id,omitempty"`
|
||
Status string `json:"status"` // running | done
|
||
ExitCode int `json:"exit_code,omitempty"`
|
||
Output string `json:"output,omitempty"`
|
||
Blackboard string `json:"blackboard,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
RoundMax int `json:"round_max"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
FinishedAt time.Time `json:"finished_at,omitempty"`
|
||
// Result 是 supervisor 写入黑板 result.json 的结构化结果(存在时优先于 Output)。
|
||
Result *CoopResult `json:"result,omitempty"`
|
||
// ChallengeCode 任务描述的题目编号(如 c-06),用于通知控制层关闭/切换靶机
|
||
ChallengeCode string `json:"challenge_code,omitempty"`
|
||
}
|
||
|
||
// CoopResult 是 pi 协作容器 supervisor 生成的标准结果($BB/result.json),
|
||
// 完成通知与主 agent 汇报轮直接使用这些结构化字段,不再依赖截断的容器 stdout。
|
||
type CoopResult struct {
|
||
Status string `json:"status"` // solved | unsolved | error | timeout
|
||
ExitCode int `json:"exit_code"`
|
||
Summary string `json:"summary"`
|
||
Flag string `json:"flag"`
|
||
Artifacts []string `json:"artifacts"`
|
||
}
|
||
|
||
// Solved 返回任务是否成功解出/交付完成。
|
||
func (r *CoopResult) Solved() bool {
|
||
return r != nil && r.Status == "solved"
|
||
}
|
||
|
||
// CoopManager 管理后台协作任务的生命周期,并在任务完成时回调 Agent,
|
||
// 由 Agent 决定是否唤醒主 agent 汇报结果。
|
||
type CoopManager struct {
|
||
mu sync.Mutex
|
||
tasks map[string]*CoopTask
|
||
notify func(task *CoopTask)
|
||
}
|
||
|
||
func NewCoopManager() *CoopManager {
|
||
return &CoopManager{tasks: make(map[string]*CoopTask)}
|
||
}
|
||
|
||
// SetNotify 注册任务完成回调。
|
||
func (m *CoopManager) SetNotify(fn func(task *CoopTask)) {
|
||
m.mu.Lock()
|
||
m.notify = fn
|
||
m.mu.Unlock()
|
||
}
|
||
|
||
// Register 登记一个正在运行的后台任务。
|
||
func (m *CoopManager) Register(task *CoopTask) {
|
||
m.mu.Lock()
|
||
m.tasks[task.ID] = task
|
||
m.mu.Unlock()
|
||
}
|
||
|
||
// Get 按任务 ID 查询任务。
|
||
func (m *CoopManager) Get(id string) (*CoopTask, bool) {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
task, ok := m.tasks[id]
|
||
return task, ok
|
||
}
|
||
|
||
// List 返回全部任务,按创建时间倒序;sessionID 非空时只返回该会话的任务。
|
||
func (m *CoopManager) List(sessionID string) []*CoopTask {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
out := make([]*CoopTask, 0, len(m.tasks))
|
||
for _, task := range m.tasks {
|
||
if sessionID == "" || task.SessionID == sessionID {
|
||
out = append(out, task)
|
||
}
|
||
}
|
||
sort.Slice(out, func(i, j int) bool {
|
||
return out[i].CreatedAt.After(out[j].CreatedAt)
|
||
})
|
||
return out
|
||
}
|
||
|
||
// Complete 标记任务完成并触发完成回调(回调在锁外执行,避免死锁)。
|
||
func (m *CoopManager) Complete(task *CoopTask) {
|
||
m.mu.Lock()
|
||
task.Status = "done"
|
||
task.FinishedAt = time.Now()
|
||
m.tasks[task.ID] = task
|
||
notify := m.notify
|
||
m.mu.Unlock()
|
||
if notify != nil {
|
||
notify(task)
|
||
}
|
||
}
|
||
|
||
// CurrentCoopRound 统计黑板 logs 目录中已出现的最大协作轮次。
|
||
// 单 agent 版日志命名为 round-N.log;兼容旧版 a-round-N.log / b-round-N.log。
|
||
func CurrentCoopRound(blackboardDir string) int {
|
||
if blackboardDir == "" {
|
||
return 0
|
||
}
|
||
entries, err := os.ReadDir(filepath.Join(blackboardDir, "logs"))
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
maxRound := 0
|
||
for _, entry := range entries {
|
||
var round int
|
||
if _, err := fmt.Sscanf(entry.Name(), "round-%d.log", &round); err == nil {
|
||
if round > maxRound {
|
||
maxRound = round
|
||
}
|
||
continue
|
||
}
|
||
if _, err := fmt.Sscanf(entry.Name(), "a-round-%d.log", &round); err == nil {
|
||
if round > maxRound {
|
||
maxRound = round
|
||
}
|
||
}
|
||
if _, err := fmt.Sscanf(entry.Name(), "b-round-%d.log", &round); err == nil {
|
||
if round > maxRound {
|
||
maxRound = round
|
||
}
|
||
}
|
||
}
|
||
return maxRound
|
||
}
|
||
|
||
// ContainerInspectStatus 查询容器当前的运行状态与人类可读状态描述
|
||
// (如 "running" + "Up 2 minutes" / "exited" + "Exited (0)"),
|
||
// 供 Web 页面实时展示协作容器状态。
|
||
func ContainerInspectStatus(socket, containerID string) (state, status string, err error) {
|
||
if containerID == "" {
|
||
return "", "", errors.New("缺少容器 ID")
|
||
}
|
||
cli, err := client.NewClientWithOpts(
|
||
client.WithHost(socket),
|
||
client.WithAPIVersionNegotiation(),
|
||
)
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
defer cli.Close()
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
list, err := cli.ContainerList(ctx, container.ListOptions{
|
||
All: true,
|
||
Filters: filters.NewArgs(filters.Arg("id", containerID)),
|
||
})
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
if len(list) == 0 {
|
||
return "", "", errors.New("容器不存在")
|
||
}
|
||
return list[0].State, list[0].Status, nil
|
||
}
|