feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更: - RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、 backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API - Windows SMB 文件传输:backend/pkg/winsmb - 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情 - Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs - 服务器导入助手:server_import_helpers、ImportCredentialSettings - 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块) - 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-* - .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
777d9e4e71
commit
76ec9387f0
202 files changed
+16555
-2543
No files matched your search
@@ -18,13 +18,42 @@ import (
|
||||
"ezops/pkg/agentcli/llm"
|
||||
"ezops/pkg/agentcli/safety"
|
||||
"ezops/pkg/agentcli/tools"
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
"ezops/pkg/remoteexec"
|
||||
)
|
||||
|
||||
// EngineConfig 引擎配置(由 controller 从 model.LLMConfig + model.Server 构建后注入) // Logan.Li
|
||||
// RunMode Agent 运行模式 // Logan.Li
|
||||
const (
|
||||
RunModeHost = "host"
|
||||
RunModeKubernetes = "kubernetes"
|
||||
)
|
||||
|
||||
// NormalizeRunMode 规范化运行模式(兼容旧 k8s 参数) // Logan.Li
|
||||
func NormalizeRunMode(mode string) string {
|
||||
if mode == "k8s" || mode == RunModeKubernetes {
|
||||
return RunModeKubernetes
|
||||
}
|
||||
if mode == "" {
|
||||
return RunModeHost
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// KubernetesTarget Kubernetes 模式目标上下文(由 controller 注入) // Logan.Li
|
||||
type KubernetesTarget struct {
|
||||
Client *k8sclient.Client
|
||||
ClusterName string
|
||||
Namespace string
|
||||
PodName string
|
||||
Container string
|
||||
}
|
||||
|
||||
// EngineConfig 引擎配置(由 controller 从 model.LLMConfig + 目标上下文构建后注入) // Logan.Li
|
||||
type EngineConfig struct {
|
||||
Mode string // host | kubernetes
|
||||
LLM llm.Config // LLM 客户端配置(endpoint 已解析)
|
||||
SSH remoteexec.SSHConfig // 目标主机 SSH 配置(含凭据)
|
||||
SSH remoteexec.SSHConfig // 主机模式 SSH 配置
|
||||
Kubernetes *KubernetesTarget // Kubernetes 模式目标
|
||||
SafetyMode safety.Mode // 安全模式(默认 balanced)
|
||||
Core *core.Config // ReAct 配置(nil 用默认)
|
||||
MemoryDir string // Evolver 记忆目录(空则 data/agentcli)
|
||||
@@ -47,25 +76,40 @@ func NewEngine(cfg EngineConfig) (*Engine, error) {
|
||||
if cfg.MemoryDir != "" {
|
||||
coreCfg.MemoryDir = cfg.MemoryDir
|
||||
}
|
||||
mode := cfg.SafetyMode
|
||||
if mode == "" {
|
||||
mode = safety.ModeBalanced
|
||||
safetyMode := cfg.SafetyMode
|
||||
if safetyMode == "" {
|
||||
safetyMode = safety.ModeBalanced
|
||||
}
|
||||
|
||||
// LLM 客户端
|
||||
client := llm.NewClient(cfg.LLM)
|
||||
|
||||
// 工具注册中心:ssh_execute(SSH 长连接)+ analyze_output
|
||||
// 工具注册中心:按模式注册互斥工具集 // Logan.Li
|
||||
registry := tools.NewRegistry()
|
||||
sshTool, err := tools.NewSSHExecuteTool(cfg.SSH, coreCfg.ToolTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh_execute 初始化失败: %w", err)
|
||||
runMode := NormalizeRunMode(cfg.Mode)
|
||||
coreCfg.Mode = runMode
|
||||
|
||||
switch runMode {
|
||||
case RunModeKubernetes:
|
||||
if cfg.Kubernetes == nil || cfg.Kubernetes.Client == nil {
|
||||
return nil, fmt.Errorf("Kubernetes 模式缺少集群客户端")
|
||||
}
|
||||
kt := cfg.Kubernetes
|
||||
registry.Register(tools.NewKubernetesGetTool(kt.Client, kt.Namespace))
|
||||
registry.Register(tools.NewPodLogsTool(kt.Client, kt.Namespace, kt.PodName, kt.Container))
|
||||
registry.Register(tools.NewPodExecuteTool(kt.Client, kt.Namespace, kt.PodName, kt.Container, coreCfg.ToolTimeout))
|
||||
registry.Register(tools.NewKubernetesActionTool(kt.Client, kt.Namespace))
|
||||
default:
|
||||
sshTool, err := tools.NewSSHExecuteTool(cfg.SSH, coreCfg.ToolTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh_execute 初始化失败: %w", err)
|
||||
}
|
||||
registry.Register(sshTool)
|
||||
}
|
||||
registry.Register(sshTool)
|
||||
registry.Register(tools.NewAnalyzeOutputTool())
|
||||
|
||||
// 安全控制器
|
||||
sc := safety.NewController(mode)
|
||||
sc := safety.NewController(safetyMode)
|
||||
|
||||
// Evolver(LLM 可用时启用学习;失败降级)
|
||||
ev, err := evolver.NewEvolverEngine(coreCfg.MemoryDir, client)
|
||||
@@ -86,9 +130,9 @@ func (e *Engine) SetConfirmFn(fn safety.ConfirmFn) { e.safety.SetConfirmFn(fn) }
|
||||
// SetAutoApprove 测试用:自动批准(生产禁用) // Logan.Li
|
||||
func (e *Engine) SetAutoApprove(b bool) { e.safety.SetAutoApprove(b) }
|
||||
|
||||
// Run 执行任务(ReAct 循环);prior 为上一轮对话历史(不含 system),priorTokens 为累计 token // Logan.Li
|
||||
func (e *Engine) Run(ctx context.Context, query, serverInfo string, prior []llm.Message, priorTokens int) (*core.RunResult, error) {
|
||||
return e.agent.Run(ctx, query, serverInfo, prior, priorTokens)
|
||||
// Run 执行任务(ReAct 循环);targetInfo 为目标上下文摘要;prior 为上一轮对话历史(不含 system) // Logan.Li
|
||||
func (e *Engine) Run(ctx context.Context, query, targetInfo string, prior []llm.Message, priorTokens int) (*core.RunResult, error) {
|
||||
return e.agent.Run(ctx, query, targetInfo, prior, priorTokens)
|
||||
}
|
||||
|
||||
// EvolveStats 进化统计(供审计展示) // Logan.Li
|
||||
|
||||
@@ -61,6 +61,7 @@ type RunResult struct {
|
||||
|
||||
// Config Agent 运行配置 // Logan.Li
|
||||
type Config struct {
|
||||
Mode string // host | kubernetes
|
||||
MaxIterations int
|
||||
Temperature float64
|
||||
ToolTimeout time.Duration
|
||||
@@ -135,7 +136,7 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.M
|
||||
|
||||
// 工具描述
|
||||
toolDescs := a.buildToolDescriptions()
|
||||
systemPrompt := prompt.BuildSystemPrompt(serverInfo, toolDescs, evolveCtx)
|
||||
systemPrompt := prompt.BuildSystemPrompt(a.config.Mode, serverInfo, toolDescs, evolveCtx)
|
||||
|
||||
messages := []llm.Message{{Role: "system", Content: systemPrompt}}
|
||||
if len(prior) > 0 {
|
||||
@@ -191,7 +192,7 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.M
|
||||
}
|
||||
messages = append(messages, llm.Message{
|
||||
Role: "user",
|
||||
Content: "【系统提示】请立即调用 ssh_execute 工具在目标主机执行命令获取实际结果,不要仅用文字描述计划。",
|
||||
Content: fmt.Sprintf("【系统提示】请立即调用 %s 工具获取实际结果,不要仅用文字描述计划。", a.primaryExecTool()),
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -303,8 +304,8 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
|
||||
success = false
|
||||
}
|
||||
|
||||
// 超长 ssh 输出自动分析,避免 token 爆炸 // Logan.Li
|
||||
if tc.Function.Name == "ssh_execute" && success && len(output) > a.autoAnalyzeThreshold() {
|
||||
// 超长命令输出自动分析,避免 token 爆炸 // Logan.Li
|
||||
if (tc.Function.Name == "ssh_execute" || tc.Function.Name == "pod_execute") && success && len(output) > a.autoAnalyzeThreshold() {
|
||||
if analyzed := a.autoAnalyzeOutput(ctx, output); analyzed != "" {
|
||||
output = analyzed
|
||||
}
|
||||
@@ -364,6 +365,14 @@ func (a *Agent) roundNum(iter int) int {
|
||||
return a.config.RoundOffset + iter + 1
|
||||
}
|
||||
|
||||
// primaryExecTool 返回当前模式下的主执行工具名 // Logan.Li
|
||||
func (a *Agent) primaryExecTool() string {
|
||||
if a.config.Mode == "kubernetes" || a.config.Mode == "k8s" {
|
||||
return "kubernetes_get"
|
||||
}
|
||||
return "ssh_execute"
|
||||
}
|
||||
|
||||
// lastToolOutput 取最近一条工具 observation,供最终答案为空时回退展示 // Logan.Li
|
||||
func lastToolOutput(messages []llm.Message) string {
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Package prompt 构建 Agent CLI 的 System Prompt(五层架构 + 动态记忆注入)。 // Logan.Li
|
||||
// 设计参考 opsxcli internal/agent/prompt,角色改写为 aiops Web 运维 Agent 语境。
|
||||
package prompt
|
||||
|
||||
import (
|
||||
@@ -7,34 +6,59 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BuildSystemPrompt 构建五层 System Prompt。 // Logan.Li
|
||||
// - Layer 1: 角色定位(aiops 智能运维 Agent)
|
||||
// - Layer 2: 安全规则(黑名单/高危确认/优先只读)
|
||||
// - Layer 3: 工具说明(由 registry 动态生成)
|
||||
// - Layer 4: 工作方式(ReAct 循环:思考→执行→观察→失败则换法重试→直到解决)
|
||||
// - Layer 5: 记忆注入(Evolver 提供的历史经验)
|
||||
//
|
||||
// serverInfo:目标主机摘要;toolDescs:工具描述文本;evolveContext:历史经验。
|
||||
func BuildSystemPrompt(serverInfo, toolDescs, evolveContext string) string {
|
||||
const runModeKubernetes = "kubernetes"
|
||||
|
||||
// normalizeMode 规范化运行模式(兼容旧 k8s,避免 import 循环) // Logan.Li
|
||||
func normalizeMode(mode string) string {
|
||||
if mode == "k8s" || mode == runModeKubernetes {
|
||||
return runModeKubernetes
|
||||
}
|
||||
if mode == "" {
|
||||
return "host"
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// BuildSystemPrompt 构建五层 System Prompt。mode: host|kubernetes // Logan.Li
|
||||
func BuildSystemPrompt(mode, targetInfo, toolDescs, evolveContext string) string {
|
||||
mode = normalizeMode(mode)
|
||||
var sb strings.Builder
|
||||
|
||||
// Layer 1: 角色
|
||||
sb.WriteString("你是 aiops 平台的 Agent CLI —— 一个企业级智能运维 Agent," +
|
||||
"类似 Claude Code / Codex,但运行在 Web 页面上,通过 SSH 工具远程操作真实服务器。\n")
|
||||
sb.WriteString("你的职责是:根据用户的自然语言任务描述,自主在目标服务器上排查故障、安装与配置软件、修复问题," +
|
||||
"并通过 WebSocket 把每一步思考、执行的命令和输出实时反馈给用户。\n\n")
|
||||
if mode == runModeKubernetes {
|
||||
sb.WriteString("你是 aiops 平台的 Agent CLI —— 一个企业级 Kubernetes 智能运维 Agent," +
|
||||
"运行在 Web 页面上,通过 kubernetes_get / pod_logs / pod_execute / kubernetes_action 操作真实 Kubernetes 集群。\n")
|
||||
sb.WriteString("你的职责是:根据用户自然语言任务,自主查询集群/命名空间资源状态、分析事件与日志、" +
|
||||
"必要时进入 Pod 容器排障,并通过 WebSocket 实时反馈每一步思考与结果。\n\n")
|
||||
} else {
|
||||
sb.WriteString("你是 aiops 平台的 Agent CLI —— 一个企业级智能运维 Agent," +
|
||||
"类似 Claude Code / Codex,但运行在 Web 页面上,通过 SSH 工具远程操作真实服务器。\n")
|
||||
sb.WriteString("你的职责是:根据用户的自然语言任务描述,自主在目标服务器上排查故障、安装与配置软件、修复问题," +
|
||||
"并通过 WebSocket 把每一步思考、执行的命令和输出实时反馈给用户。\n\n")
|
||||
}
|
||||
|
||||
// 目标主机
|
||||
if strings.TrimSpace(serverInfo) != "" {
|
||||
sb.WriteString("【目标主机】\n" + serverInfo + "\n\n")
|
||||
// 目标上下文
|
||||
if strings.TrimSpace(targetInfo) != "" {
|
||||
label := "目标主机"
|
||||
if mode == runModeKubernetes {
|
||||
label = "目标集群上下文"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("【%s】\n%s\n\n", label, targetInfo))
|
||||
}
|
||||
|
||||
// Layer 2: 安全规则
|
||||
sb.WriteString("【安全规则】\n")
|
||||
sb.WriteString("1. 破坏性命令(rm -rf /、mkfs、dd 覆写磁盘等)被严格禁止,绝不执行。\n")
|
||||
sb.WriteString("2. 高危操作(reboot、systemctl restart、kill、卸载软件等)需要用户确认后才能执行。\n")
|
||||
sb.WriteString("3. 排障时优先使用只读命令(df/ps/cat/grep/tail/systemctl status/journalctl)收集信息,确认根因后再动手修复。\n")
|
||||
sb.WriteString("4. 不要执行任何你不确定后果的命令;宁可多问用户,也不盲目操作。\n\n")
|
||||
sb.WriteString("2. 高危操作(删除 Pod、重启 Deployment、扩缩容、换镜像等)需要用户确认后才能执行。\n")
|
||||
if mode == runModeKubernetes {
|
||||
sb.WriteString("3. 排障时优先使用只读工具:kubernetes_get 查资源状态、pod_logs 看日志;确认根因后再 pod_execute 或 kubernetes_action。\n")
|
||||
sb.WriteString("4. 容器内可能没有 systemctl、宿主机路径;不要假设节点级命令可用。\n")
|
||||
sb.WriteString("5. 未选定 Pod 时,用 kubernetes_get 和 pod_logs(带 pod 参数)排障;选定 Pod 后可直接 pod_execute。\n")
|
||||
} else {
|
||||
sb.WriteString("3. 排障时优先使用只读命令(df/ps/cat/grep/tail/systemctl status/journalctl)收集信息,确认根因后再动手修复。\n")
|
||||
sb.WriteString("4. 不要执行任何你不确定后果的命令;宁可多问用户,也不盲目操作。\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Layer 3: 工具
|
||||
if strings.TrimSpace(toolDescs) != "" {
|
||||
@@ -45,20 +69,24 @@ func BuildSystemPrompt(serverInfo, toolDescs, evolveContext string) string {
|
||||
sb.WriteString("【工作方式(ReAct 循环)】\n")
|
||||
sb.WriteString("你采用「思考-行动-观察」循环解决任务:\n")
|
||||
sb.WriteString("1. 思考:分析当前状态,决定下一步。在回复中先用简短文字说明你的判断和计划。\n")
|
||||
sb.WriteString("2. 行动:调用 ssh_execute 执行命令(一次只做关键的、可验证的一步)。\n")
|
||||
sb.WriteString("3. 观察:仔细阅读命令输出(含 exit_code,0 表示成功)。\n")
|
||||
sb.WriteString("4. 循环:如果命令失败或问题未解决,分析错误原因,换一种方法重试,直到真正解决。不要因一次失败就放弃。\n")
|
||||
if mode == runModeKubernetes {
|
||||
sb.WriteString("2. 行动:优先 kubernetes_get / pod_logs;需要容器内信息时用 pod_execute;变更用 kubernetes_action(一次一步)。\n")
|
||||
sb.WriteString("3. 观察:仔细阅读工具输出;pod_execute 输出含 [exit_code=0] 表示成功。\n")
|
||||
} else {
|
||||
sb.WriteString("2. 行动:调用 ssh_execute 执行命令(一次只做关键的、可验证的一步)。\n")
|
||||
sb.WriteString("3. 观察:仔细阅读命令输出(含 exit_code,0 表示成功)。\n")
|
||||
}
|
||||
sb.WriteString("4. 循环:如果失败或问题未解决,分析原因,换一种方法重试,直到真正解决。不要因一次失败就放弃。\n")
|
||||
sb.WriteString("5. 收尾:问题解决后,用清晰的中文总结:问题根因、你做了什么、最终结果、后续建议。然后停止调用工具。\n\n")
|
||||
|
||||
sb.WriteString("【重要】务必真正解决问题,而不是假装解决。每一步都要基于实际命令输出判断," +
|
||||
"禁止编造不存在的输出。命令超时或连接失败时,向用户说明并建议排查方向。\n\n")
|
||||
sb.WriteString("【重要】务必真正解决问题,而不是假装解决。每一步都要基于实际工具输出判断," +
|
||||
"禁止编造不存在的输出。超时或 API 失败时,向用户说明并建议排查方向。\n\n")
|
||||
|
||||
// Layer 5: 记忆注入
|
||||
if strings.TrimSpace(evolveContext) != "" {
|
||||
sb.WriteString("【历史经验(来自自进化引擎)】\n" + evolveContext + "\n\n")
|
||||
}
|
||||
|
||||
// 输出要求
|
||||
sb.WriteString("【输出要求】使用简体中文,简洁专业。思考与命令穿插呈现,避免冗长空话。")
|
||||
return sb.String()
|
||||
}
|
||||
@@ -70,4 +98,4 @@ func FormatToolDescriptions(items []struct{ Name, Desc string }) string {
|
||||
sb.WriteString(fmt.Sprintf("- %s: %s\n", it.Name, it.Desc))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func (t *AnalyzeOutputTool) Name() string { return "analyze_output" }
|
||||
// Description 给 LLM 的说明 // Logan.Li
|
||||
func (t *AnalyzeOutputTool) Description() string {
|
||||
return "分析一段(可能很长的)命令输出文本:统计行数、提取疑似错误/告警行、必要时截断保留首尾。" +
|
||||
"当 ssh_execute 返回内容过长时调用此工具提炼关键信息再继续推理。"
|
||||
"当 ssh_execute 或 pod_execute 返回内容过长时调用此工具提炼关键信息再继续推理。"
|
||||
}
|
||||
|
||||
// Parameters 参数定义 // Logan.Li
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// kubernetes_action.go - Kubernetes 集群变更操作(高风险,需用户确认) // Logan.Li
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
)
|
||||
|
||||
// KubernetesActionTool 执行 Kubernetes 变更动作 // Logan.Li
|
||||
type KubernetesActionTool struct {
|
||||
client *k8sclient.Client
|
||||
namespace string
|
||||
}
|
||||
|
||||
// NewKubernetesActionTool 创建变更工具 // Logan.Li
|
||||
func NewKubernetesActionTool(client *k8sclient.Client, namespace string) *KubernetesActionTool {
|
||||
return &KubernetesActionTool{client: client, namespace: namespace}
|
||||
}
|
||||
|
||||
// Name 工具名 // Logan.Li
|
||||
func (t *KubernetesActionTool) Name() string { return "kubernetes_action" }
|
||||
|
||||
// Description 给 LLM 的说明 // Logan.Li
|
||||
func (t *KubernetesActionTool) Description() string {
|
||||
return "对 Kubernetes 资源执行变更操作(高风险,需用户确认)。" +
|
||||
"action 支持: restart_deployment, scale_deployment, update_image, restart_statefulset, delete_pod, suspend_cronjob。" +
|
||||
"变更前请先用 kubernetes_get 确认当前状态。"
|
||||
}
|
||||
|
||||
// Parameters JSON Schema // Logan.Li
|
||||
func (t *KubernetesActionTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "操作类型: restart_deployment|scale_deployment|update_image|restart_statefulset|delete_pod|suspend_cronjob",
|
||||
},
|
||||
"namespace": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "命名空间,默认会话绑定命名空间",
|
||||
},
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "资源名称(Deployment/StatefulSet/CronJob/Pod)",
|
||||
},
|
||||
"replicas": map[string]interface{}{
|
||||
"type": "integer",
|
||||
"description": "scale_deployment 目标副本数",
|
||||
},
|
||||
"image": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "update_image 新镜像地址",
|
||||
},
|
||||
"container": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "update_image 目标容器名,可省略(默认第一个)",
|
||||
},
|
||||
"suspend": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "suspend_cronjob: true=暂停, false=恢复",
|
||||
},
|
||||
},
|
||||
"required": []string{"action", "name"},
|
||||
}
|
||||
}
|
||||
|
||||
// RiskLevel 静态高风险 // Logan.Li
|
||||
func (t *KubernetesActionTool) RiskLevel() RiskLevel { return RiskHigh }
|
||||
|
||||
// Execute 执行变更 // Logan.Li
|
||||
func (t *KubernetesActionTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) {
|
||||
action := strings.ToLower(strings.TrimSpace(StringParam(args, "action")))
|
||||
name := strings.TrimSpace(StringParam(args, "name"))
|
||||
ns := strings.TrimSpace(StringParam(args, "namespace"))
|
||||
if ns == "" {
|
||||
ns = t.namespace
|
||||
}
|
||||
if action == "" || name == "" {
|
||||
return &Result{Success: false, Error: "action 和 name 不能为空"}, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var msg string
|
||||
switch action {
|
||||
case "restart_deployment":
|
||||
err = t.client.RestartDeployment(ns, name)
|
||||
msg = fmt.Sprintf("已触发 Deployment %s/%s 滚动重启", ns, name)
|
||||
case "scale_deployment":
|
||||
replicas := IntParam(args, "replicas")
|
||||
if replicas < 0 {
|
||||
return &Result{Success: false, Error: "replicas 不能为负数"}, nil
|
||||
}
|
||||
err = t.client.ScaleDeployment(ns, name, replicas)
|
||||
msg = fmt.Sprintf("已将 Deployment %s/%s 扩缩容至 %d 副本", ns, name, replicas)
|
||||
case "update_image":
|
||||
image := strings.TrimSpace(StringParam(args, "image"))
|
||||
if image == "" {
|
||||
return &Result{Success: false, Error: "update_image 需要 image 参数"}, nil
|
||||
}
|
||||
container := strings.TrimSpace(StringParam(args, "container"))
|
||||
err = t.client.UpdateDeploymentImage(ns, name, container, image)
|
||||
msg = fmt.Sprintf("已更新 Deployment %s/%s 镜像为 %s", ns, name, image)
|
||||
case "restart_statefulset":
|
||||
err = t.client.RestartStatefulSet(ns, name)
|
||||
msg = fmt.Sprintf("已触发 StatefulSet %s/%s 滚动重启", ns, name)
|
||||
case "delete_pod":
|
||||
err = t.client.DeletePod(ns, name)
|
||||
msg = fmt.Sprintf("已删除 Pod %s/%s(控制器将视情况重建)", ns, name)
|
||||
case "suspend_cronjob":
|
||||
suspend := true
|
||||
if v, ok := args["suspend"].(bool); ok {
|
||||
suspend = v
|
||||
}
|
||||
err = t.client.SuspendCronJob(ns, name, suspend)
|
||||
if suspend {
|
||||
msg = fmt.Sprintf("已暂停 CronJob %s/%s", ns, name)
|
||||
} else {
|
||||
msg = fmt.Sprintf("已恢复 CronJob %s/%s", ns, name)
|
||||
}
|
||||
default:
|
||||
return &Result{Success: false, Error: fmt.Sprintf("不支持的 action: %s", action)}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return &Result{Success: false, Error: fmt.Sprintf("操作失败: %v", err)}, nil
|
||||
}
|
||||
return &Result{Success: true, Output: msg}, nil
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
// kubernetes_get.go - Kubernetes 集群/命名空间资源统一只读查询 // Logan.Li
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
)
|
||||
|
||||
// KubernetesGetTool 统一只读查询工具 // Logan.Li
|
||||
type KubernetesGetTool struct {
|
||||
client *k8sclient.Client
|
||||
namespace string // 会话默认命名空间
|
||||
}
|
||||
|
||||
// NewKubernetesGetTool 创建查询工具 // Logan.Li
|
||||
func NewKubernetesGetTool(client *k8sclient.Client, namespace string) *KubernetesGetTool {
|
||||
return &KubernetesGetTool{client: client, namespace: namespace}
|
||||
}
|
||||
|
||||
// Name 工具名 // Logan.Li
|
||||
func (t *KubernetesGetTool) Name() string { return "kubernetes_get" }
|
||||
|
||||
// Description 给 LLM 的说明 // Logan.Li
|
||||
func (t *KubernetesGetTool) Description() string {
|
||||
return "查询 Kubernetes 集群或命名空间资源状态(只读)。" +
|
||||
"resource 支持: nodes, pods, deployments, statefulsets, daemonsets, services, ingresses, events, namespaces, hpa, cronjobs。" +
|
||||
"用于盘点节点/Pod 健康、副本状态、事件告警、服务路由等。返回结构化摘要,非原始 JSON。"
|
||||
}
|
||||
|
||||
// Parameters JSON Schema // Logan.Li
|
||||
func (t *KubernetesGetTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"resource": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "资源类型: nodes|pods|deployments|statefulsets|daemonsets|services|ingresses|events|namespaces|hpa|cronjobs",
|
||||
},
|
||||
"namespace": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "命名空间;pods/deployments 等命名空间级资源需要;空则用会话绑定命名空间",
|
||||
},
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "可选,查询单个资源名",
|
||||
},
|
||||
"label_selector": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "可选,标签筛选,格式 app=nginx 或 app=nginx,env=prod",
|
||||
},
|
||||
"status_filter": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "可选,Pod 状态筛选,如 Running、Pending、CrashLoopBackOff、NotRunning",
|
||||
},
|
||||
},
|
||||
"required": []string{"resource"},
|
||||
}
|
||||
}
|
||||
|
||||
// RiskLevel 只读 // Logan.Li
|
||||
func (t *KubernetesGetTool) RiskLevel() RiskLevel { return RiskSafe }
|
||||
|
||||
// Execute 执行查询 // Logan.Li
|
||||
func (t *KubernetesGetTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) {
|
||||
resource := strings.ToLower(strings.TrimSpace(StringParam(args, "resource")))
|
||||
if resource == "" {
|
||||
return &Result{Success: false, Error: "resource 不能为空"}, nil
|
||||
}
|
||||
ns := strings.TrimSpace(StringParam(args, "namespace"))
|
||||
if ns == "" {
|
||||
ns = t.namespace
|
||||
}
|
||||
name := strings.TrimSpace(StringParam(args, "name"))
|
||||
labelSel := parseLabelSelector(StringParam(args, "label_selector"))
|
||||
statusFilter := strings.TrimSpace(StringParam(args, "status_filter"))
|
||||
|
||||
var output string
|
||||
var err error
|
||||
switch resource {
|
||||
case "nodes", "node":
|
||||
output, err = t.formatNodes(name)
|
||||
case "pods", "pod":
|
||||
output, err = t.formatPods(ns, name, labelSel, statusFilter)
|
||||
case "deployments", "deployment", "deploy":
|
||||
output, err = t.formatDeployments(ns, name)
|
||||
case "statefulsets", "statefulset", "sts":
|
||||
output, err = t.formatStatefulSets(ns, name)
|
||||
case "daemonsets", "daemonset", "ds":
|
||||
output, err = t.formatDaemonSets(ns, name)
|
||||
case "services", "service", "svc":
|
||||
output, err = t.formatServices(ns, name)
|
||||
case "ingresses", "ingress", "ing":
|
||||
output, err = t.formatIngresses(ns, name)
|
||||
case "events", "event":
|
||||
output, err = t.formatEvents(ns, name)
|
||||
case "namespaces", "namespace", "ns":
|
||||
output, err = t.formatNamespaces(name)
|
||||
case "hpa", "hpas":
|
||||
output, err = t.formatHPAs(ns, name)
|
||||
case "cronjobs", "cronjob":
|
||||
output, err = t.formatCronJobs(ns, name)
|
||||
default:
|
||||
return &Result{Success: false, Error: fmt.Sprintf("不支持的 resource: %s", resource)}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return &Result{Success: false, Error: err.Error()}, nil
|
||||
}
|
||||
return &Result{Success: true, Output: output}, nil
|
||||
}
|
||||
|
||||
func parseLabelSelector(raw string) map[string]string {
|
||||
out := make(map[string]string)
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return out
|
||||
}
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if kv := strings.SplitN(part, "=", 2); len(kv) == 2 {
|
||||
out[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func labelsMatch(labels, selector map[string]string) bool {
|
||||
if len(selector) == 0 {
|
||||
return true
|
||||
}
|
||||
for k, v := range selector {
|
||||
if labels[k] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func podDisplayStatus(p k8sclient.PodItem) string {
|
||||
phase := p.Status.Phase
|
||||
for _, cs := range p.Status.ContainerStatuses {
|
||||
if !cs.Ready && cs.RestartCount > 0 {
|
||||
return "CrashLoopBackOff"
|
||||
}
|
||||
}
|
||||
for _, c := range p.Status.Conditions {
|
||||
if c.Type == "Ready" && c.Status != "True" && c.Reason != "" {
|
||||
return c.Reason
|
||||
}
|
||||
}
|
||||
return phase
|
||||
}
|
||||
|
||||
func matchPodStatus(status, filter string) bool {
|
||||
if filter == "" {
|
||||
return true
|
||||
}
|
||||
f := strings.ToLower(filter)
|
||||
s := strings.ToLower(status)
|
||||
if f == "notrunning" {
|
||||
return s != "running" && s != "succeeded" && s != "completed"
|
||||
}
|
||||
return strings.Contains(s, f)
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatNodes(name string) (string, error) {
|
||||
list, err := t.client.ListNodes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Nodes (%d):\n", len(list.Items)))
|
||||
for _, n := range list.Items {
|
||||
if name != "" && n.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
ready := "Unknown"
|
||||
for _, c := range n.Status.Conditions {
|
||||
if c.Type == "Ready" {
|
||||
ready = c.Status
|
||||
break
|
||||
}
|
||||
}
|
||||
ip := ""
|
||||
for _, a := range n.Status.Addresses {
|
||||
if a.Type == "InternalIP" {
|
||||
ip = a.Address
|
||||
break
|
||||
}
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("- %s Ready=%s IP=%s OS=%s kubelet=%s\n",
|
||||
n.Metadata.Name, ready, ip, n.Status.NodeInfo.OSImage, n.Status.NodeInfo.KubeletVersion))
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatPods(ns, name string, labelSel map[string]string, statusFilter string) (string, error) {
|
||||
list, err := t.client.ListPods()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
type row struct{ line string }
|
||||
var rows []row
|
||||
for _, p := range list.Items {
|
||||
if ns != "" && p.Metadata.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
if name != "" && p.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
if !labelsMatch(p.Metadata.Labels, labelSel) {
|
||||
continue
|
||||
}
|
||||
st := podDisplayStatus(p)
|
||||
if !matchPodStatus(st, statusFilter) {
|
||||
continue
|
||||
}
|
||||
image := ""
|
||||
if len(p.Spec.Containers) > 0 {
|
||||
image = p.Spec.Containers[0].Image
|
||||
}
|
||||
restarts := 0
|
||||
if len(p.Status.ContainerStatuses) > 0 {
|
||||
restarts = p.Status.ContainerStatuses[0].RestartCount
|
||||
}
|
||||
rows = append(rows, row{line: fmt.Sprintf("- %s/%s %s restarts=%d node=%s ip=%s image=%s\n",
|
||||
p.Metadata.Namespace, p.Metadata.Name, st, restarts, p.Spec.NodeName, p.Status.PodIP, image)})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].line < rows[j].line })
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Pods (%d):\n", len(rows)))
|
||||
for _, r := range rows {
|
||||
sb.WriteString(r.line)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
sb.WriteString("(无匹配 Pod)\n")
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatDeployments(ns, name string) (string, error) {
|
||||
list, err := t.client.ListDeployments()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var sb strings.Builder
|
||||
count := 0
|
||||
sb.WriteString("Deployments:\n")
|
||||
for _, d := range list.Items {
|
||||
if ns != "" && d.Metadata.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
if name != "" && d.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
sb.WriteString(fmt.Sprintf("- %s/%s replicas=%d/%d ready=%d available=%d paused=%v\n",
|
||||
d.Metadata.Namespace, d.Metadata.Name,
|
||||
d.Status.ReadyReplicas, d.Spec.Replicas,
|
||||
d.Status.ReadyReplicas, d.Status.AvailableReplicas, d.Spec.Paused))
|
||||
}
|
||||
if count == 0 {
|
||||
sb.WriteString("(无匹配 Deployment)\n")
|
||||
} else {
|
||||
sb.Reset()
|
||||
sb.WriteString(fmt.Sprintf("Deployments (%d):\n", count))
|
||||
for _, d := range list.Items {
|
||||
if ns != "" && d.Metadata.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
if name != "" && d.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("- %s/%s replicas=%d/%d ready=%d available=%d paused=%v\n",
|
||||
d.Metadata.Namespace, d.Metadata.Name,
|
||||
d.Status.ReadyReplicas, d.Spec.Replicas,
|
||||
d.Status.ReadyReplicas, d.Status.AvailableReplicas, d.Spec.Paused))
|
||||
}
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatStatefulSets(ns, name string) (string, error) {
|
||||
list, err := t.client.ListStatefulSets()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var lines []string
|
||||
for _, s := range list.Items {
|
||||
if ns != "" && s.Metadata.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
if name != "" && s.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("- %s/%s replicas=%d/%d ready=%d\n",
|
||||
s.Metadata.Namespace, s.Metadata.Name, s.Status.ReadyReplicas, s.Spec.Replicas, s.Status.ReadyReplicas))
|
||||
}
|
||||
sort.Strings(lines)
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("StatefulSets (%d):\n", len(lines)))
|
||||
if len(lines) == 0 {
|
||||
sb.WriteString("(无匹配 StatefulSet)\n")
|
||||
} else {
|
||||
for _, l := range lines {
|
||||
sb.WriteString(l + "\n")
|
||||
}
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatDaemonSets(ns, name string) (string, error) {
|
||||
list, err := t.client.ListDaemonSets()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var lines []string
|
||||
for _, d := range list.Items {
|
||||
if ns != "" && d.Metadata.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
if name != "" && d.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("- %s/%s desired=%d ready=%d\n",
|
||||
d.Metadata.Namespace, d.Metadata.Name, d.Status.DesiredNumberScheduled,
|
||||
d.Status.NumberReady))
|
||||
}
|
||||
sort.Strings(lines)
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("DaemonSets (%d):\n", len(lines)))
|
||||
for _, l := range lines {
|
||||
sb.WriteString(l + "\n")
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatServices(ns, name string) (string, error) {
|
||||
list, err := t.client.ListServices()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var lines []string
|
||||
for _, s := range list.Items {
|
||||
if ns != "" && s.Metadata.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
if name != "" && s.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
ports := ""
|
||||
for i, p := range s.Spec.Ports {
|
||||
if i > 0 {
|
||||
ports += ","
|
||||
}
|
||||
ports += fmt.Sprintf("%d", p.Port)
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("- %s/%s type=%s clusterIP=%s ports=[%s] selector=%v\n",
|
||||
s.Metadata.Namespace, s.Metadata.Name, s.Spec.Type, s.Spec.ClusterIP, ports, s.Spec.Selector))
|
||||
}
|
||||
sort.Strings(lines)
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Services (%d):\n", len(lines)))
|
||||
for _, l := range lines {
|
||||
sb.WriteString(l)
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatIngresses(ns, name string) (string, error) {
|
||||
list, err := t.client.ListIngresses()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var lines []string
|
||||
for _, ing := range list.Items {
|
||||
if ns != "" && ing.Metadata.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
if name != "" && ing.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
hosts := []string{}
|
||||
for _, r := range ing.Spec.Rules {
|
||||
if r.Host != "" {
|
||||
hosts = append(hosts, r.Host)
|
||||
}
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("- %s/%s hosts=%v\n", ing.Metadata.Namespace, ing.Metadata.Name, hosts))
|
||||
}
|
||||
sort.Strings(lines)
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Ingresses (%d):\n", len(lines)))
|
||||
for _, l := range lines {
|
||||
sb.WriteString(l + "\n")
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatEvents(ns, involvedName string) (string, error) {
|
||||
list, err := t.client.ListEvents(ns)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
type evRow struct {
|
||||
ts time.Time
|
||||
line string
|
||||
}
|
||||
var rows []evRow
|
||||
for _, e := range list.Items {
|
||||
if involvedName != "" && e.InvolvedObject.Name != involvedName {
|
||||
continue
|
||||
}
|
||||
ts := e.LastTimestamp
|
||||
if ts.IsZero() {
|
||||
ts = e.EventTime
|
||||
}
|
||||
rows = append(rows, evRow{
|
||||
ts: ts,
|
||||
line: fmt.Sprintf("[%s] %s %s/%s: %s — %s (x%d)\n",
|
||||
e.Type, e.Reason, e.InvolvedObject.Kind, e.InvolvedObject.Name, e.Message,
|
||||
e.InvolvedObject.Namespace, e.Count),
|
||||
})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].ts.After(rows[j].ts) })
|
||||
if len(rows) > 30 {
|
||||
rows = rows[:30]
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Events (recent %d):\n", len(rows)))
|
||||
for _, r := range rows {
|
||||
sb.WriteString(r.line)
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatNamespaces(name string) (string, error) {
|
||||
names, err := t.client.ListNamespaces()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sort.Strings(names)
|
||||
var sb strings.Builder
|
||||
count := 0
|
||||
sb.WriteString("Namespaces:\n")
|
||||
for _, n := range names {
|
||||
if name != "" && n != name {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
sb.WriteString(fmt.Sprintf("- %s\n", n))
|
||||
}
|
||||
if count == 0 {
|
||||
sb.WriteString("(无匹配命名空间)\n")
|
||||
} else {
|
||||
sb.Reset()
|
||||
sb.WriteString(fmt.Sprintf("Namespaces (%d):\n", count))
|
||||
for _, n := range names {
|
||||
if name != "" && n != name {
|
||||
continue
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("- %s\n", n))
|
||||
}
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatHPAs(ns, name string) (string, error) {
|
||||
list, err := t.client.ListHPAs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var lines []string
|
||||
for _, h := range list.Items {
|
||||
if ns != "" && h.Metadata.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
if name != "" && h.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("- %s/%s target=%s/%s min=%d max=%d current=%d\n",
|
||||
h.Metadata.Namespace, h.Metadata.Name,
|
||||
h.Spec.ScaleTargetRef.Kind, h.Spec.ScaleTargetRef.Name,
|
||||
h.Spec.MinReplicas, h.Spec.MaxReplicas, h.Status.CurrentReplicas))
|
||||
}
|
||||
sort.Strings(lines)
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("HPAs (%d):\n", len(lines)))
|
||||
for _, l := range lines {
|
||||
sb.WriteString(l)
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (t *KubernetesGetTool) formatCronJobs(ns, name string) (string, error) {
|
||||
list, err := t.client.ListCronJobs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var lines []string
|
||||
for _, cj := range list.Items {
|
||||
if ns != "" && cj.Metadata.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
if name != "" && cj.Metadata.Name != name {
|
||||
continue
|
||||
}
|
||||
suspended := false
|
||||
if cj.Spec.Suspend != nil {
|
||||
suspended = *cj.Spec.Suspend
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("- %s/%s schedule=%s suspend=%v active=%d\n",
|
||||
cj.Metadata.Namespace, cj.Metadata.Name, cj.Spec.Schedule, suspended, len(cj.Status.Active)))
|
||||
}
|
||||
sort.Strings(lines)
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("CronJobs (%d):\n", len(lines)))
|
||||
for _, l := range lines {
|
||||
sb.WriteString(l)
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// pod_execute.go - 在目标 Pod 容器内执行 shell 命令 // Logan.Li
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
)
|
||||
|
||||
// PodExecuteTool 在 Pod 容器内执行命令(K8s exec WebSocket 协议) // Logan.Li
|
||||
type PodExecuteTool struct {
|
||||
client *k8sclient.Client
|
||||
namespace string
|
||||
podName string
|
||||
container string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewPodExecuteTool 创建 Pod 执行工具 // Logan.Li
|
||||
func NewPodExecuteTool(client *k8sclient.Client, namespace, podName, container string, timeout time.Duration) *PodExecuteTool {
|
||||
if timeout <= 0 {
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
return &PodExecuteTool{
|
||||
client: client, namespace: namespace, podName: podName,
|
||||
container: container, timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Name 工具名 // Logan.Li
|
||||
func (t *PodExecuteTool) Name() string { return "pod_execute" }
|
||||
|
||||
// Description 给 LLM 的工具说明 // Logan.Li
|
||||
func (t *PodExecuteTool) Description() string {
|
||||
return "在已选定的 Kubernetes Pod 容器内执行 shell 命令并返回 stdout+stderr。" +
|
||||
"用于容器内排障(查看进程/配置/连通性)、应用调试。" +
|
||||
"注意:容器内可能没有 systemctl 或宿主机路径;多容器 Pod 可通过 container 参数指定。" +
|
||||
"高危命令需用户确认。"
|
||||
}
|
||||
|
||||
// Parameters JSON Schema // Logan.Li
|
||||
func (t *PodExecuteTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"command": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要执行的 shell 命令,例如 ps aux、cat /etc/nginx/nginx.conf、curl -s localhost:8080/health",
|
||||
},
|
||||
"container": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "可选,多容器 Pod 时指定容器名;默认使用会话绑定的容器",
|
||||
},
|
||||
"timeout": map[string]interface{}{
|
||||
"type": "integer",
|
||||
"description": "命令超时秒数(默认 60)",
|
||||
},
|
||||
},
|
||||
"required": []string{"command"},
|
||||
}
|
||||
}
|
||||
|
||||
// RiskLevel 静态风险 // Logan.Li
|
||||
func (t *PodExecuteTool) RiskLevel() RiskLevel { return RiskHigh }
|
||||
|
||||
// RiskLevelForArgs 依据命令动态评估风险 // Logan.Li
|
||||
func (t *PodExecuteTool) RiskLevelForArgs(args map[string]interface{}) RiskLevel {
|
||||
return AssessBashRisk(StringParam(args, "command"))
|
||||
}
|
||||
|
||||
// Execute 在 Pod 内执行命令 // Logan.Li
|
||||
func (t *PodExecuteTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) {
|
||||
command := strings.TrimSpace(StringParam(args, "command"))
|
||||
if command == "" {
|
||||
return &Result{Success: false, Error: "参数 command 不能为空"}, nil
|
||||
}
|
||||
if t.podName == "" {
|
||||
return &Result{Success: false, Error: "未绑定目标 Pod,请在前端选择 Pod 或在命令前先 kubernetes_get 确认"}, nil
|
||||
}
|
||||
|
||||
container := strings.TrimSpace(StringParam(args, "container"))
|
||||
if container == "" {
|
||||
container = t.container
|
||||
}
|
||||
|
||||
timeout := t.timeout
|
||||
if sec := IntParam(args, "timeout"); sec > 0 {
|
||||
timeout = time.Duration(sec) * time.Second
|
||||
}
|
||||
|
||||
execCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
// 用 sh -c 保持 shell 语义,并嵌入 exit_code 与 ssh_execute 对齐 // Logan.Li
|
||||
wrapped := command + "; echo [exit_code=$?]"
|
||||
done := make(chan struct {
|
||||
res *k8sclient.ExecResult
|
||||
err error
|
||||
}, 1)
|
||||
go func() {
|
||||
r, err := t.client.ExecInPod(t.namespace, t.podName, container, []string{"sh", "-c", wrapped})
|
||||
done <- struct {
|
||||
res *k8sclient.ExecResult
|
||||
err error
|
||||
}{r, err}
|
||||
}()
|
||||
|
||||
var result *k8sclient.ExecResult
|
||||
var err error
|
||||
select {
|
||||
case <-execCtx.Done():
|
||||
return &Result{Success: false, Error: "命令执行超时"}, nil
|
||||
case out := <-done:
|
||||
result, err = out.res, out.err
|
||||
}
|
||||
if err != nil {
|
||||
return &Result{Success: false, Error: fmt.Sprintf("Pod exec 失败: %v", err)}, nil
|
||||
}
|
||||
|
||||
output := strings.TrimSpace(result.Stdout)
|
||||
if result.Stderr != "" {
|
||||
if output != "" {
|
||||
output += "\n"
|
||||
}
|
||||
output += strings.TrimSpace(result.Stderr)
|
||||
}
|
||||
success := strings.Contains(output, "[exit_code=0]")
|
||||
if !success && !strings.Contains(output, "[exit_code=") {
|
||||
// 无 exit_code 标记时,无 stderr 视为成功 // Logan.Li
|
||||
success = strings.TrimSpace(result.Stderr) == ""
|
||||
}
|
||||
|
||||
return &Result{Success: success, Output: output}, nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// pod_logs.go - 获取 Pod 容器日志(只读) // Logan.Li
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
)
|
||||
|
||||
// PodLogsTool 拉取 Pod 日志 // Logan.Li
|
||||
type PodLogsTool struct {
|
||||
client *k8sclient.Client
|
||||
namespace string
|
||||
podName string
|
||||
container string
|
||||
}
|
||||
|
||||
// NewPodLogsTool 创建日志工具 // Logan.Li
|
||||
func NewPodLogsTool(client *k8sclient.Client, namespace, podName, container string) *PodLogsTool {
|
||||
return &PodLogsTool{client: client, namespace: namespace, podName: podName, container: container}
|
||||
}
|
||||
|
||||
// Name 工具名 // Logan.Li
|
||||
func (t *PodLogsTool) Name() string { return "pod_logs" }
|
||||
|
||||
// Description 给 LLM 的说明 // Logan.Li
|
||||
func (t *PodLogsTool) Description() string {
|
||||
return "获取目标 Pod 容器的最近日志(只读)。排障时优先使用此工具,比 pod_execute tail 更高效。" +
|
||||
"未选 Pod 时需通过 name 参数指定 Pod 名。"
|
||||
}
|
||||
|
||||
// Parameters JSON Schema // Logan.Li
|
||||
func (t *PodLogsTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"pod": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Pod 名称;会话已绑定 Pod 时可省略",
|
||||
},
|
||||
"container": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "容器名;多容器时指定,默认会话绑定容器",
|
||||
},
|
||||
"tail_lines": map[string]interface{}{
|
||||
"type": "integer",
|
||||
"description": "返回最近行数,默认 200,最大 2000",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RiskLevel 只读安全 // Logan.Li
|
||||
func (t *PodLogsTool) RiskLevel() RiskLevel { return RiskSafe }
|
||||
|
||||
// Execute 拉取日志 // Logan.Li
|
||||
func (t *PodLogsTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) {
|
||||
pod := strings.TrimSpace(StringParam(args, "pod"))
|
||||
if pod == "" {
|
||||
pod = t.podName
|
||||
}
|
||||
if pod == "" {
|
||||
return &Result{Success: false, Error: "请指定 pod 参数或在前端选择目标 Pod"}, nil
|
||||
}
|
||||
|
||||
container := strings.TrimSpace(StringParam(args, "container"))
|
||||
if container == "" {
|
||||
container = t.container
|
||||
}
|
||||
|
||||
tail := IntParam(args, "tail_lines")
|
||||
if tail <= 0 {
|
||||
tail = 200
|
||||
}
|
||||
if tail > 2000 {
|
||||
tail = 2000
|
||||
}
|
||||
|
||||
logs, err := t.client.GetPodLogs(t.namespace, pod, container, tail)
|
||||
if err != nil {
|
||||
return &Result{Success: false, Error: fmt.Sprintf("获取日志失败: %v", err)}, nil
|
||||
}
|
||||
if strings.TrimSpace(logs) == "" {
|
||||
return &Result{Success: true, Output: "(日志为空)"}, nil
|
||||
}
|
||||
return &Result{Success: true, Output: logs}, nil
|
||||
}
|
||||
Reference in new issue
Block a user