Files
2026-08-14 23:41:57 +08:00

321 lines
9.5 KiB
Go

package agent
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"sort"
"strconv"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
)
// Docker 工具:通过 Docker API 操作本地或远程 Docker。
// socket 地址通过 DockerConfigStore 动态读取,用户在 Web 页面配置后即时生效。
func (t *Toolset) dockerClient() (*client.Client, error) {
socket := t.dockerSocket()
cli, err := client.NewClientWithOpts(
client.WithHost(socket),
client.WithAPIVersionNegotiation(),
)
if err != nil {
return nil, fmt.Errorf("创建 Docker 客户端失败: %w", err)
}
return cli, nil
}
func (t *Toolset) dockerPS(ctx context.Context, params map[string]any) ToolResult {
all, _ := params["all"].(bool)
cli, err := t.dockerClient()
if err != nil {
return ToolResult{Success: false, Output: err.Error()}
}
defer cli.Close()
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
defer cancel()
list, err := cli.ContainerList(ctx, container.ListOptions{All: all})
if err != nil {
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
}
if len(list) == 0 {
return ToolResult{Success: true, Output: "没有找到容器"}
}
sort.Slice(list, func(i, j int) bool {
return list[i].Created > list[j].Created
})
var out strings.Builder
fmt.Fprintf(&out, "%-14s %-26s %-20s %-12s %s\n", "CONTAINER ID", "NAME", "IMAGE", "STATUS", "PORTS")
for _, c := range list {
name := strings.TrimPrefix(strings.Join(c.Names, ","), "/")
fmt.Fprintf(&out, "%-14s %-26s %-20s %-12s %s\n",
c.ID[:min(12, len(c.ID))], name, c.Image, c.Status, dockerPorts(c.Ports))
}
return ToolResult{Success: true, Output: t.truncate(out.String())}
}
func (t *Toolset) dockerImages(ctx context.Context, params map[string]any) ToolResult {
cli, err := t.dockerClient()
if err != nil {
return ToolResult{Success: false, Output: err.Error()}
}
defer cli.Close()
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
defer cancel()
list, err := cli.ImageList(ctx, image.ListOptions{All: true})
if err != nil {
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
}
if len(list) == 0 {
return ToolResult{Success: true, Output: "没有找到镜像"}
}
sort.Slice(list, func(i, j int) bool {
return list[i].Created > list[j].Created
})
var out strings.Builder
fmt.Fprintf(&out, "%-22s %-28s %-14s %s\n", "REPOSITORY", "TAG", "IMAGE ID", "SIZE")
for _, img := range list {
if len(img.RepoTags) == 0 {
continue
}
for _, tag := range img.RepoTags {
repo, imageTag, _ := strings.Cut(tag, ":")
id := img.ID
if len(id) > 12 {
id = id[7:19]
}
fmt.Fprintf(&out, "%-22s %-28s %-14s %s\n", repo, imageTag, id, formatBytes(img.Size))
}
}
return ToolResult{Success: true, Output: t.truncate(out.String())}
}
func (t *Toolset) dockerLogs(ctx context.Context, params map[string]any) ToolResult {
containerID, _ := params["container"].(string)
containerID = strings.TrimSpace(containerID)
if containerID == "" {
return ToolResult{Success: false, Output: "container 不能为空"}
}
tail := intParam(params, "tail")
if tail <= 0 {
tail = 100
}
cli, err := t.dockerClient()
if err != nil {
return ToolResult{Success: false, Output: err.Error()}
}
defer cli.Close()
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
defer cancel()
resp, err := cli.ContainerLogs(ctx, containerID, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Tail: strconv.Itoa(tail),
})
if err != nil {
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
}
defer resp.Close()
raw, err := io.ReadAll(resp)
if err != nil {
return ToolResult{Success: false, Output: "读取 Docker 日志失败: " + err.Error()}
}
return ToolResult{Success: true, Output: t.truncate(demuxDockerLogs(raw))}
}
// demuxDockerLogs 解析 Docker 日志流:多路复用流(stdout/stderr 分帧)按帧解出,
// 普通文本流(TTY 容器)直接返回。
func demuxDockerLogs(data []byte) string {
if len(data) >= 8 && isMultiplexedFrame(data[0:8]) {
var out strings.Builder
for i := 0; i+8 <= len(data); {
frameSize := int(binary.BigEndian.Uint32(data[i+4 : i+8]))
i += 8
if frameSize < 0 || i+frameSize > len(data) {
break
}
out.Write(data[i : i+frameSize])
i += frameSize
}
return out.String()
}
return string(data)
}
func isMultiplexedFrame(header []byte) bool {
streamType := header[0]
if streamType != 0 && streamType != 1 && streamType != 2 {
return false
}
return header[1] == 0 && header[2] == 0 && header[3] == 0
}
func (t *Toolset) dockerInspect(ctx context.Context, params map[string]any) ToolResult {
containerID, _ := params["container"].(string)
containerID = strings.TrimSpace(containerID)
if containerID == "" {
return ToolResult{Success: false, Output: "container 不能为空"}
}
cli, err := t.dockerClient()
if err != nil {
return ToolResult{Success: false, Output: err.Error()}
}
defer cli.Close()
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
defer cancel()
inspect, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
}
state := inspect.State
if state == nil {
state = &container.State{}
}
summary := map[string]any{
"id": inspect.ID,
"name": strings.TrimPrefix(inspect.Name, "/"),
"image": inspect.Config.Image,
"status": state.Status,
"running": state.Running,
"exit_code": state.ExitCode,
"restart_count": inspect.RestartCount,
"created": inspect.Created,
"command": inspect.Config.Cmd,
"env_count": len(inspect.Config.Env),
"ports": inspect.NetworkSettings.Ports,
"network_mode": string(inspect.HostConfig.NetworkMode),
"ip_address": inspect.NetworkSettings.IPAddress,
}
data, _ := json.MarshalIndent(summary, "", " ")
return ToolResult{Success: true, Output: t.truncate(string(data))}
}
func (t *Toolset) dockerExec(ctx context.Context, params map[string]any) ToolResult {
containerID, _ := params["container"].(string)
containerID = strings.TrimSpace(containerID)
command, _ := params["command"].(string)
command = strings.TrimSpace(command)
if containerID == "" || command == "" {
return ToolResult{Success: false, Output: "container 和 command 不能为空"}
}
cli, err := t.dockerClient()
if err != nil {
return ToolResult{Success: false, Output: err.Error()}
}
defer cli.Close()
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
defer cancel()
execCfg := container.ExecOptions{
AttachStdout: true,
AttachStderr: true,
Cmd: []string{"sh", "-c", command},
}
created, err := cli.ContainerExecCreate(ctx, containerID, execCfg)
if err != nil {
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
}
attach, err := cli.ContainerExecAttach(ctx, created.ID, container.ExecAttachOptions{})
if err != nil {
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
}
defer attach.Close()
raw, err := io.ReadAll(attach.Reader)
if err != nil {
return ToolResult{Success: false, Output: "读取命令输出失败: " + err.Error()}
}
return ToolResult{Success: true, Output: t.truncate(demuxDockerLogs(raw))}
}
func (t *Toolset) dockerStart(ctx context.Context, params map[string]any) ToolResult {
containerID, _ := params["container"].(string)
containerID = strings.TrimSpace(containerID)
if containerID == "" {
return ToolResult{Success: false, Output: "container 不能为空"}
}
cli, err := t.dockerClient()
if err != nil {
return ToolResult{Success: false, Output: err.Error()}
}
defer cli.Close()
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
defer cancel()
if err := cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
}
return ToolResult{Success: true, Output: "容器 " + containerID + " 已启动"}
}
func (t *Toolset) dockerStop(ctx context.Context, params map[string]any) ToolResult {
containerID, _ := params["container"].(string)
containerID = strings.TrimSpace(containerID)
if containerID == "" {
return ToolResult{Success: false, Output: "container 不能为空"}
}
timeoutSeconds := intParam(params, "timeout")
cli, err := t.dockerClient()
if err != nil {
return ToolResult{Success: false, Output: err.Error()}
}
defer cli.Close()
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
defer cancel()
var timeout *int
if timeoutSeconds > 0 {
timeout = &timeoutSeconds
}
if err := cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: timeout}); err != nil {
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
}
return ToolResult{Success: true, Output: "容器 " + containerID + " 已停止"}
}
func dockerPorts(ports []container.Port) string {
if len(ports) == 0 {
return ""
}
var parts []string
for _, p := range ports {
host := ""
if p.PublicPort > 0 {
host = fmt.Sprintf("%d->", p.PublicPort)
}
parts = append(parts, fmt.Sprintf("%s%d/%s", host, p.PrivatePort, p.Type))
}
return strings.Join(parts, ", ")
}
func formatBytes(size int64) string {
const unit = 1024
if size < unit {
return fmt.Sprintf("%dB", size)
}
div, exp := int64(unit), 0
for n := size / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f%cB", float64(size)/float64(div), "KMGTPE"[exp])
}