feat: 完善堡垒机与 Agent 智能运维能力

- 支持 Windows Agent 构建、安装脚本和统一上报协议

- 增强 RDP 剪贴板、虚拟盘和文件传输能力

- 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口

- 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
Logan.Li committed 2026-07-05 23:49:36 +08:00
1 parent a9513c2056
commit e0d652d211
169 files changed
+12192 -9116

No files matched your search

+8 -3
View File
@@ -10,14 +10,14 @@ LDFLAGS := -s -w -X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME)
# 静态编译:关闭 CGO 并使用静态链接
LINUX_LDFLAGS := -extldflags "-static"
.PHONY: build cross build-linux-amd64 build-linux-arm64 build-darwin-arm64 tidy run fmt vet clean help
.PHONY: build cross build-linux-amd64 build-linux-arm64 build-darwin-arm64 build-windows-amd64 tidy run fmt vet clean help
## build: 本地构建当前平台二进制
build:
go build -ldflags "$(LDFLAGS)" -o $(APP) .
## cross: 交叉编译 Linux amd64/arm64 + macOS arm64
cross: build-linux-amd64 build-linux-arm64 build-darwin-arm64
## cross: 交叉编译 Linux amd64/arm64 + macOS arm64 + Windows amd64 // Logan.Li
cross: build-linux-amd64 build-linux-arm64 build-darwin-arm64 build-windows-amd64
## build-linux-amd64: 交叉编译 linux/amd64
build-linux-amd64:
@@ -34,6 +34,11 @@ build-darwin-arm64:
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \
go build -ldflags "$(LDFLAGS)" -o bin/$(APP)-darwin-arm64 .
## build-windows-amd64: 编译 Windows/amd64 Agent // Logan.Li
build-windows-amd64:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
go build -ldflags "$(LDFLAGS)" -o bin/$(APP)-windows-amd64.exe .
## tidy: 整理依赖
tidy:
go mod tidy
+17 -1
View File
@@ -141,6 +141,22 @@ curl -o /usr/local/bin/aiops-agent http://10.10.10.75:8080/agent/aiops-agent-lin
curl -fsSL http://10.10.10.75:8080/agent/install.sh | sudo bash -s -- --server http://10.10.10.75:8080 --token <TOKEN>
```
### Windows
Windows Agent 使用同一套 `/api/v1/agent/report` 上报协议,主机监控图表不需要单独分叉。第一版已支持 CPU、内存、逻辑磁盘、监听端口、TCP 连接数、进程 TOP、系统信息、健康探针和 PowerShell 任务执行;Linux 专属的 Load、IOWait、inode、FD、磁盘 IO 等字段在 Windows 上会保持 0 或空值。// Logan.Li
在管理员 PowerShell 中执行:
```powershell
powershell -ExecutionPolicy Bypass -Command "iwr http://10.10.10.75:8080/agent/install.ps1 -UseBasicParsing -OutFile $env:TEMP\install-aiops-agent.ps1; & $env:TEMP\install-aiops-agent.ps1 -Server 'http://10.10.10.75:8080' -Token '<TOKEN>'"
```
手动下载:
```powershell
Invoke-WebRequest -Uri http://10.10.10.75:8080/agent/aiops-agent-windows-amd64.exe -OutFile C:\ProgramData\AIOps\aiops-agent.exe -UseBasicParsing
```
## 本地构建
```bash
@@ -148,4 +164,4 @@ cd agent
make # CGO_ENABLED=0 静态编译当前平台
```
Docker 多架构构建由顶层 `backend/Dockerfile` 的 `agent-builder` stage 完成(linux-amd64 / linux-arm64 / darwin-arm64)。
顶层 `./run.sh build agent` 会交叉编译 `linux-amd64`、`linux-arm64`、`darwin-arm64`、`windows-amd64.exe`。Docker 镜像会携带当前目标架构 Linux Agent 和 Windows AMD64 Agent。// Logan.Li
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 的上下文切换采集器。
// 读取 /proc/stat 中的 ctxt(上下文切换)和 intr(中断)累计值。
// 注意:此处返回累计值而非速率,后端可用两次上报差值计算。
+15
View File
@@ -0,0 +1,15 @@
//go:build windows
package collector
import "ezops/agent/model"
type ContextSwitchCollector struct{}
func NewContextSwitchCollector() *ContextSwitchCollector {
return &ContextSwitchCollector{}
}
func (c *ContextSwitchCollector) Collect() (*model.ContextSwitchStat, error) {
return nil, nil
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现 CPU 使用率与负载采集,主要通过解析 Linux /proc/stat 与 /proc/loadavg 实现。
//
+77
View File
@@ -0,0 +1,77 @@
//go:build windows
// Package collector 提供 Windows CPU 指标采集能力。 // Logan.Li
package collector
import (
"fmt"
"runtime"
"sync"
"syscall"
"unsafe"
"ezops/agent/model"
)
type filetime struct {
LowDateTime uint32
HighDateTime uint32
}
type cpuSample struct {
total uint64
idle uint64
iowait uint64
}
type CPUCollector struct {
mu sync.Mutex
prev *cpuSample
}
func NewCPUCollector() *CPUCollector {
return &CPUCollector{}
}
func (c *CPUCollector) Collect() (model.CPUStat, error) {
stat := model.CPUStat{CoreCount: runtime.NumCPU()}
cur, err := readCPUSample()
if err != nil {
return stat, fmt.Errorf("读取 Windows CPU 采样失败: %w", err)
}
c.mu.Lock()
defer c.mu.Unlock()
if c.prev != nil {
totalDelta := cur.total - c.prev.total
idleDelta := cur.idle - c.prev.idle
if totalDelta > 0 {
stat.UsagePercent = float64(totalDelta-idleDelta) / float64(totalDelta) * 100.0
if stat.UsagePercent < 0 {
stat.UsagePercent = 0
}
}
}
c.prev = &cur
return stat, nil
}
func readCPUSample() (cpuSample, error) {
var idle, kernel, user filetime
proc := syscall.NewLazyDLL("kernel32.dll").NewProc("GetSystemTimes")
ret, _, err := proc.Call(
uintptr(unsafe.Pointer(&idle)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if ret == 0 {
return cpuSample{}, err
}
idleTicks := filetimeToUint64(idle)
kernelTicks := filetimeToUint64(kernel)
userTicks := filetimeToUint64(user)
return cpuSample{total: kernelTicks + userTicks, idle: idleTicks}, nil
}
func filetimeToUint64(ft filetime) uint64 {
return uint64(ft.HighDateTime)<<32 | uint64(ft.LowDateTime)
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现对所有已挂载的真实文件系统执行 statfs,采集磁盘使用情况。
// 使用 golang.org/x/sys/unix 的 Statfs,兼容 Linux 与 macOS。
+52
View File
@@ -0,0 +1,52 @@
//go:build windows
// Package collector 提供 Windows 逻辑磁盘使用率采集能力。 // Logan.Li
package collector
import (
"fmt"
"syscall"
"unsafe"
"ezops/agent/model"
)
type DiskCollector struct{}
func NewDiskCollector() *DiskCollector {
return &DiskCollector{}
}
func (d *DiskCollector) Collect() ([]model.DiskStat, error) {
mask, _, err := syscall.NewLazyDLL("kernel32.dll").NewProc("GetLogicalDrives").Call()
if mask == 0 {
return nil, fmt.Errorf("GetLogicalDrives 失败: %w", err)
}
var disks []model.DiskStat
for i := 0; i < 26; i++ {
if mask&(1<<uint(i)) == 0 {
continue
}
root := string(rune('A'+i)) + `:\`
var freeAvail, totalBytes, totalFree uint64
pathPtr, _ := syscall.UTF16PtrFromString(root)
ret, _, _ := syscall.NewLazyDLL("kernel32.dll").NewProc("GetDiskFreeSpaceExW").Call(
uintptr(unsafe.Pointer(pathPtr)),
uintptr(unsafe.Pointer(&freeAvail)),
uintptr(unsafe.Pointer(&totalBytes)),
uintptr(unsafe.Pointer(&totalFree)),
)
if ret == 0 || totalBytes == 0 {
continue
}
used := totalBytes - totalFree
disks = append(disks, model.DiskStat{
Mount: root,
Device: root,
TotalGB: bytesToGB(totalBytes),
UsedGB: bytesToGB(used),
UsagePercent: float64(used) / float64(totalBytes) * 100.0,
})
}
return disks, nil
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector — 磁盘 IO 指标采集器
// 通过读取 /proc/diskstats 获取磁盘读写 BPS、IOPS 和 util(磁盘繁忙程度)。
//
+15
View File
@@ -0,0 +1,15 @@
//go:build windows
package collector
import "ezops/agent/model"
type DiskIOCollector struct{}
func NewDiskIOCollector() *DiskIOCollector {
return &DiskIOCollector{}
}
func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
return model.DiskIOStat{}, nil
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 的 FD 句柄采集器。
// 读取 /proc/sys/fs/file-nr 和 /proc/sys/fs/file-max 获取系统级文件句柄使用情况。
//
+15
View File
@@ -0,0 +1,15 @@
//go:build windows
package collector
import "ezops/agent/model"
type FDCollector struct{}
func NewFDCollector() *FDCollector {
return &FDCollector{}
}
func (c *FDCollector) Collect() (*model.SocketFDStat, error) {
return nil, nil
}
+9 -1
View File
@@ -12,6 +12,7 @@ import (
"net"
"net/http"
"os/exec"
"runtime"
"strings"
"time"
@@ -193,7 +194,7 @@ func (h *HealthProbeCollector) execCmd(parentCtx context.Context, cfg model.Prob
ctx, cancel := context.WithTimeout(parentCtx, timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", cfg.ExecCommand)
cmd := execProbeCommand(ctx, cfg.ExecCommand)
output, err := cmd.CombinedOutput()
latencyMs := int(time.Since(start).Milliseconds())
result.LatencyMs = latencyMs
@@ -214,3 +215,10 @@ func (h *HealthProbeCollector) execCmd(parentCtx context.Context, cfg model.Prob
result.Message = fmt.Sprintf("命令执行成功 (%dms)", latencyMs)
return result
}
func execProbeCommand(ctx context.Context, command string) *exec.Cmd {
if runtime.GOOS == "windows" {
return exec.CommandContext(ctx, "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command)
}
return exec.CommandContext(ctx, "sh", "-c", command)
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现内存使用率采集,通过解析 Linux /proc/meminfo 获取。
//
+56
View File
@@ -0,0 +1,56 @@
//go:build windows
// Package collector 提供 Windows 内存指标采集能力。 // Logan.Li
package collector
import (
"fmt"
"syscall"
"unsafe"
"ezops/agent/model"
)
type memoryStatusEx struct {
Length uint32
MemoryLoad uint32
TotalPhys uint64
AvailPhys uint64
TotalPageFile uint64
AvailPageFile uint64
TotalVirtual uint64
AvailVirtual uint64
AvailExtendedVirtual uint64
}
type MemCollector struct{}
func NewMemCollector() *MemCollector {
return &MemCollector{}
}
func (m *MemCollector) Collect() (model.MemoryStat, error) {
var ms memoryStatusEx
ms.Length = uint32(unsafe.Sizeof(ms))
ret, _, err := syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx").Call(uintptr(unsafe.Pointer(&ms)))
if ret == 0 {
return model.MemoryStat{}, fmt.Errorf("GlobalMemoryStatusEx 失败: %w", err)
}
total := ms.TotalPhys
avail := ms.AvailPhys
used := total - avail
swapTotal := ms.TotalPageFile
swapAvail := ms.AvailPageFile
swapUsed := uint64(0)
if swapTotal > swapAvail {
swapUsed = swapTotal - swapAvail
}
stat := model.MemoryStat{
TotalGB: bytesToGB(total),
UsedGB: bytesToGB(used),
AvailableGB: bytesToGB(avail),
SwapUsedGB: bytesToGB(swapUsed),
UsagePercent: float64(ms.MemoryLoad),
}
return stat, nil
}
+7 -5
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现网络连接数、网卡流量等网络指标采集。
//
@@ -18,11 +20,11 @@ import (
// netSample 保存上一次的网络采样数据,用于计算速率
type netSample struct {
time time.Time
rxBytes uint64
txBytes uint64
rxErrors uint64
txErrors uint64
time time.Time
rxBytes uint64
txBytes uint64
rxErrors uint64
txErrors uint64
}
// NetworkCollector 网络指标采集器
+53
View File
@@ -0,0 +1,53 @@
//go:build windows
// Package collector 提供 Windows 网络连接基础采集能力。 // Logan.Li
package collector
import (
"bytes"
"os/exec"
"strings"
"ezops/agent/model"
)
type NetworkCollector struct{}
func NewNetworkCollector() *NetworkCollector {
return &NetworkCollector{}
}
func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
stat := model.NetworkStat{NicName: firstWindowsNIC()}
out, err := exec.Command("netstat", "-ano", "-p", "tcp").Output()
if err != nil {
return stat, nil
}
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
if len(fields) < 4 || strings.ToUpper(fields[0]) != "TCP" {
continue
}
state := strings.ToUpper(fields[3])
switch state {
case "ESTABLISHED":
stat.TCPEstablished++
case "TIME_WAIT":
stat.TCPTimeWait++
case "CLOSE_WAIT":
stat.TCPCloseWait++
}
stat.TCPTotal++
}
return stat, nil
}
func firstWindowsNIC() string {
out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", "Get-NetAdapter | Where-Object {$_.Status -eq 'Up'} | Select-Object -First 1 -ExpandProperty Name").Output()
if err == nil {
if name := strings.TrimSpace(string(bytes.TrimSpace(out))); name != "" {
return name
}
}
return "Ethernet"
}
+3 -1
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现监听 TCP 端口的自动发现:
// - 扫描 /proc/net/tcp 与 /proc/net/tcp6 中状态为 LISTEN(0A) 的套接字;
@@ -35,7 +37,7 @@ var systemProcesses = map[string]bool{
"upowerd": true,
"rtkit-daemon": true,
"switcheroo-contr": true,
"power-profiles-": true, // power-profiles-daemon
"power-profiles-": true, // power-profiles-daemon
"wpa_supplicant": true,
"avahi-daemon": true,
"cron": true,
+94
View File
@@ -0,0 +1,94 @@
//go:build windows
// Package collector 提供 Windows 监听端口发现能力。 // Logan.Li
package collector
import (
"encoding/csv"
"os/exec"
"strconv"
"strings"
"ezops/agent/model"
)
type PortCollector struct {
Policy *model.PortPolicy
}
func NewPortCollector() *PortCollector {
return &PortCollector{}
}
func (p *PortCollector) SetPolicy(policy *model.PortPolicy) {
p.Policy = policy
}
func (p *PortCollector) Collect() ([]model.PortStat, error) {
pidNames := windowsProcessNames()
out, err := exec.Command("netstat", "-ano", "-p", "tcp").Output()
if err != nil {
return []model.PortStat{}, nil
}
seen := map[string]bool{}
var ports []model.PortStat
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
if len(fields) < 5 || strings.ToUpper(fields[0]) != "TCP" || strings.ToUpper(fields[3]) != "LISTENING" {
continue
}
port := parseWindowsPort(fields[1])
if port <= 0 {
continue
}
if p.Policy != nil && p.Policy.IsBlacklistMode() && !p.Policy.InWhitelist(port) {
continue
}
pid, _ := strconv.Atoi(fields[4])
key := strconv.Itoa(port) + "|" + strconv.Itoa(pid)
if seen[key] {
continue
}
seen[key] = true
ports = append(ports, model.PortStat{
Port: port,
Protocol: "tcp",
Process: pidNames[pid],
PID: pid,
})
}
return ports, nil
}
func parseWindowsPort(addr string) int {
addr = strings.Trim(addr, "[]")
idx := strings.LastIndex(addr, ":")
if idx < 0 || idx+1 >= len(addr) {
return 0
}
port, _ := strconv.Atoi(addr[idx+1:])
return port
}
func windowsProcessNames() map[int]string {
out, err := exec.Command("tasklist", "/FO", "CSV", "/NH").Output()
if err != nil {
return map[int]string{}
}
reader := csv.NewReader(strings.NewReader(string(out)))
rows, err := reader.ReadAll()
if err != nil {
return map[int]string{}
}
result := map[int]string{}
for _, row := range rows {
if len(row) < 2 {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(row[1]))
if err == nil {
result[pid] = row[0]
}
}
return result
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector — 进程 TOP 采集器
// 扫描 /proc 获取 TOP N 进程完整信息:PID/User/CMD/CPU%/MEM%/FDs
// 阿里云 ECS 监控风格 — 直观 6 列布局
+62
View File
@@ -0,0 +1,62 @@
//go:build windows
// Package collector 提供 Windows 进程 TOP 基础采集能力。 // Logan.Li
package collector
import (
"encoding/csv"
"os/exec"
"sort"
"strconv"
"strings"
"ezops/agent/model"
)
type ProcessTopCollector struct{}
func NewProcessTopCollector() *ProcessTopCollector {
return &ProcessTopCollector{}
}
func (p *ProcessTopCollector) Collect(topN int) ([]model.ProcessStat, error) {
out, err := exec.Command("tasklist", "/FO", "CSV", "/NH").Output()
if err != nil {
return []model.ProcessStat{}, nil
}
rows, err := csv.NewReader(strings.NewReader(string(out))).ReadAll()
if err != nil {
return []model.ProcessStat{}, nil
}
var procs []model.ProcessStat
for _, row := range rows {
if len(row) < 5 {
continue
}
pid, _ := strconv.Atoi(strings.TrimSpace(row[1]))
memKB := parseWindowsMemKB(row[4])
procs = append(procs, model.ProcessStat{
PID: pid,
Name: row[0],
Cmdline: row[0],
User: "",
MemoryMB: float64(memKB) / 1024.0,
})
}
sort.Slice(procs, func(i, j int) bool {
return procs[i].MemoryMB > procs[j].MemoryMB
})
if topN > len(procs) {
topN = len(procs)
}
if topN < 0 {
topN = 0
}
return procs[:topN], nil
}
func parseWindowsMemKB(s string) int64 {
clean := strings.NewReplacer(",", "", "K", "", "k", "", " ", "").Replace(s)
v, _ := strconv.ParseInt(clean, 10, 64)
return v
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector — Swap 采集器
// 从 /proc/meminfo 读取 SwapTotal / SwapFree
// 仅当系统有 swap 分区时返回有效数据
+26
View File
@@ -0,0 +1,26 @@
//go:build windows
package collector
import "ezops/agent/model"
type SwapCollector struct{}
func NewSwapCollector() *SwapCollector {
return &SwapCollector{}
}
func (s *SwapCollector) Collect() (*model.SwapStat, error) {
return nil, nil
}
func parseUint64(s string) (uint64, error) {
var v uint64
for _, c := range s {
if c < '0' || c > '9' {
break
}
v = v*10 + uint64(c-'0')
}
return v, nil
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 实现 AIOps Agent 系统静态信息采集。
// 仅在 Agent 启动时执行一次,采集 OS 版本、内核版本等信息。
//
+56
View File
@@ -0,0 +1,56 @@
//go:build windows
// Package collector 实现 Windows 系统静态信息采集。 // Logan.Li
package collector
import (
"os/exec"
"strings"
)
type SystemInfo struct {
OSVersion string
Kernel string
UptimeSeconds int64
}
func CollectSystem() SystemInfo {
return SystemInfo{
OSVersion: windowsOSCaption(),
Kernel: windowsVersion(),
UptimeSeconds: windowsUptime(),
}
}
func windowsOSCaption() string {
out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", "(Get-CimInstance Win32_OperatingSystem).Caption").Output()
if err != nil {
return "Windows"
}
if s := strings.TrimSpace(string(out)); s != "" {
return s
}
return "Windows"
}
func windowsVersion() string {
out, err := exec.Command("cmd.exe", "/C", "ver").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func windowsUptime() int64 {
out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", "[int]((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime).TotalSeconds").Output()
if err != nil {
return 0
}
var n int64
for _, ch := range strings.TrimSpace(string(out)) {
if ch >= '0' && ch <= '9' {
n = n*10 + int64(ch-'0')
}
}
return n
}
+8
View File
@@ -0,0 +1,8 @@
//go:build windows
package collector
func bytesToGB(b uint64) float64 {
const gb = 1024.0 * 1024.0 * 1024.0
return float64(b) / gb
}
+71
View File
@@ -0,0 +1,71 @@
param(
[Parameter(Mandatory = $true)]
[string]$Server,
[Parameter(Mandatory = $false)]
[string]$Token = "none",
[Parameter(Mandatory = $false)]
[int]$Interval = 10
)
$ErrorActionPreference = "Stop"
function Assert-Admin {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "请使用管理员 PowerShell 执行安装脚本"
}
}
function Normalize-Server([string]$Value) {
return $Value.TrimEnd("/")
}
Assert-Admin
$Server = Normalize-Server $Server
$InstallDir = "C:\Program Files\AIOps"
$LogDir = "C:\ProgramData\AIOps\logs"
$AgentPath = Join-Path $InstallDir "aiops-agent.exe"
$ServiceName = "AIOpsAgent"
$BinaryUrl = "$Server/agent/aiops-agent-windows-amd64.exe"
Write-Host ""
Write-Host "AIOps Agent Windows 安装"
Write-Host "Server: $Server"
Write-Host "Binary: $BinaryUrl"
Write-Host ""
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
$Tmp = Join-Path $env:TEMP "aiops-agent-windows-amd64.exe"
Invoke-WebRequest -Uri $BinaryUrl -OutFile $Tmp -UseBasicParsing
Copy-Item -Force $Tmp $AgentPath
$Args = "-server `"$Server`" -interval $Interval -token `"$Token`""
$Existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($Existing) {
Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
sc.exe delete $ServiceName | Out-Null
Start-Sleep -Seconds 1
}
New-Service -Name $ServiceName `
-DisplayName "AIOps Agent" `
-Description "AIOps Windows monitoring and task agent" `
-BinaryPathName "`"$AgentPath`" $Args" `
-StartupType Automatic | Out-Null
Start-Service -Name $ServiceName
Start-Sleep -Seconds 2
$Status = (Get-Service -Name $ServiceName).Status
if ($Status -ne "Running") {
throw "服务启动失败,当前状态: $Status"
}
Write-Host ""
Write-Host "安装成功,AIOps Agent 已作为 Windows 服务启动"
Write-Host "查看状态: Get-Service AIOpsAgent"
Write-Host "停止服务: Stop-Service AIOpsAgent"
Write-Host "卸载服务: Stop-Service AIOpsAgent; sc.exe delete AIOpsAgent"
+2 -2
View File
@@ -63,8 +63,8 @@ func (c *Client) PollProbes(ctx context.Context) ([]model.ProbeConfig, error) {
path := probePath + "?hostname=" + url.QueryEscape(c.hostname)
var wrapper struct {
Code int `json:"code"`
Message string `json:"message"`
Code int `json:"code"`
Message string `json:"message"`
Data []model.ProbeConfig `json:"data"`
}
if err := c.client.DoJSON(ctx, "GET", path, nil, &wrapper); err != nil {
+2 -22
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package task 实现 AIOps Agent 的任务拉取与执行能力。
// Agent 通过轮询接口从后端获取待执行任务(命令/脚本),本地执行后回传结果。
//
@@ -13,28 +15,6 @@ import (
"time"
)
// 任务类型常量
const (
TypeCommand = "command" // 单条 shell 命令
TypeScript = "script" // 多行 bash 脚本
)
// Task 表示从后端拉取到的待执行任务。
type Task struct {
TaskID string `json:"task_id"` // 任务唯一标识,回传结果时使用
Type string `json:"type"` // 任务类型:command / script
Command string `json:"command"` // 命令内容(command 时为单条命令,script 时为脚本正文)
Timeout int `json:"timeout"` // 执行超时,单位:秒;<=0 表示不限制
}
// Result 表示任务执行结果,用于回传后端。
type Result struct {
TaskID string `json:"task_id"` // 对应的任务 ID
ExitCode int `json:"exit_code"` // 进程退出码;超时/启动失败时为 -1
Result string `json:"result"` // 合并后的 stdout+stderr 输出
Status string `json:"status"` // 执行状态:success / failed / timeout
}
// Executor 负责本地执行任务并产出结果。
type Executor struct {
logger *log.Logger
+103
View File
@@ -0,0 +1,103 @@
//go:build windows
// Package task 实现 Windows Agent 的 PowerShell 任务执行能力。 // Logan.Li
package task
import (
"bytes"
"context"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"time"
)
type Executor struct {
logger *log.Logger
}
func New(logger *log.Logger) *Executor {
return &Executor{logger: logger}
}
func (e *Executor) ExecuteTask(ctx context.Context, t Task) Result {
res := Result{TaskID: t.TaskID}
execCtx, cancel := e.withTimeout(ctx, t.Timeout)
defer cancel()
var cmd *exec.Cmd
var cleanup func()
switch t.Type {
case TypeCommand:
cmd = exec.CommandContext(execCtx, "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", t.Command)
case TypeScript:
path, err := writeTempPowerShell(t.Command)
if err != nil {
res.ExitCode = -1
res.Status = "failed"
res.Result = "写入临时 PowerShell 脚本失败: " + err.Error()
return res
}
cleanup = func() { _ = os.Remove(path) }
cmd = exec.CommandContext(execCtx, "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path)
default:
res.ExitCode = -1
res.Status = "failed"
res.Result = fmt.Sprintf("未知的任务类型: %q", t.Type)
return res
}
if cleanup != nil {
defer cleanup()
}
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
start := time.Now()
err := cmd.Run()
elapsed := time.Since(start)
if execCtx.Err() == context.DeadlineExceeded {
res.ExitCode = -1
res.Status = "timeout"
res.Result = fmt.Sprintf("任务执行超时(%ds),已输出:\n%s", t.Timeout, buf.String())
e.logger.Printf("任务 %s 执行超时(%ds)", t.TaskID, t.Timeout)
return res
}
res.ExitCode = exitCode(err)
res.Result = buf.String()
if res.ExitCode == 0 {
res.Status = "success"
e.logger.Printf("任务 %s 执行成功 exit=0 耗时=%s 输出=%dB", t.TaskID, elapsed, buf.Len())
} else {
res.Status = "failed"
e.logger.Printf("任务 %s 执行失败 exit=%d 耗时=%s 输出=%dB", t.TaskID, res.ExitCode, elapsed, buf.Len())
}
return res
}
func (e *Executor) withTimeout(parent context.Context, timeout int) (context.Context, context.CancelFunc) {
if timeout <= 0 {
return parent, func() {}
}
return context.WithTimeout(parent, time.Duration(timeout)*time.Second)
}
func exitCode(err error) int {
if err == nil {
return 0
}
if exitErr, ok := err.(*exec.ExitError); ok {
return exitErr.ExitCode()
}
return -1
}
func writeTempPowerShell(script string) (string, error) {
path := filepath.Join(os.TempDir(), fmt.Sprintf("aiops-agent-task-%d.ps1", time.Now().UnixNano()))
if err := os.WriteFile(path, []byte(script), 0600); err != nil {
return "", err
}
return path, nil
}
+23
View File
@@ -0,0 +1,23 @@
// Package task 定义 Agent 远程任务类型。 // Logan.Li
package task
const (
TypeCommand = "command" // 单条 shell/PowerShell 命令 // Logan.Li
TypeScript = "script" // 多行脚本 // Logan.Li
)
// Task 表示从后端拉取到的待执行任务。 // Logan.Li
type Task struct {
TaskID string `json:"task_id"`
Type string `json:"type"`
Command string `json:"command"`
Timeout int `json:"timeout"`
}
// Result 表示任务执行结果,用于回传后端。 // Logan.Li
type Result struct {
TaskID string `json:"task_id"`
ExitCode int `json:"exit_code"`
Result string `json:"result"`
Status string `json:"status"`
}