feat: 新增 Agent CLI — Web 版 AI 运维 Agent(参考 opsxcli 移植)

后端引擎 backend/pkg/agentcli/(纯引擎库,零业务依赖,剥离 CLI/TUI 外壳):
- llm: OpenAI 兼容客户端,复用 model.LLMConfig
- tools: ssh_execute(复用 pkg/remoteexec)+ analyze_output + 风险评估黑名单
- safety: 三级安全模式 + 五级风险 + WebSocket 异步确认
- prompt: 五层 System Prompt
- core: ReAct 16 轮循环 + 自适应检查点 + 循环检测
- evolver: 四层记忆 + 进化循环,data/agentcli/ 文件存储

集成层:
- controller/agent_cli.go: WebSocket 控制器(事件流推流 + 高危确认 + 会话持久化)
- model/agent_cli.go: agent_cli_sessions/messages 表 + AutoMigrate
- 路由 /ws/agent-cli + /api/v1/aiops/agent-cli/sessions
- seed 权限 aiops:agent-cli / aiops:llm-config

前端:
- AgentCLI.tsx: 对话流 + 工具块 + 高危确认 + 四模块 tips 面板
- LLMConfig.tsx: 大模型设置独立页
- api/agentCli.ts + Sidebar/App 菜单路由

文档:
- docs/agent/ 11 篇(opsxcli 同名文档适配改写 + 00-README 映射总览)

其他:
- CLAUDE.md 补充 agent-cli 模块说明
- 修正 .gitignore 根级二进制规则(前导 /,避免误伤 frontend/src/components/aiops/)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
attackerandClaude committed 2026-06-27 09:55:17 +08:00
1 parent f2069fe3f0
commit 92a2247390
37 files changed
+6449 -6

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
// openai.go - OpenAI 兼容 LLM 客户端实现 // Logan.Li
// 复用 aiops 既有 LLM 调用模式(model.LLMConfig + resolveEndpoint + /chat/completions + Bearer)。
// 支持所有 OpenAI 兼容后端:openai / deepseek / qwen / moonshot / ollama / custom。
package llm
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Config LLM 客户端配置(endpoint 已由上层解析为完整 base url) // Logan.Li
type Config struct {
Endpoint string // 完整 base url,如 https://api.deepseek.com/v1
APIKey string // Bearer token
Model string // 模型名,如 deepseek-chat / gpt-4o-mini
Temperature float64 // 采样温度
MaxTokens int // 单次最大输出 token
Timeout time.Duration // HTTP 超时(0 表示使用默认 180s)
}
// OpenAIClient OpenAI 兼容客户端 // Logan.Li
type OpenAIClient struct {
cfg Config
client *http.Client
}
// NewClient 创建 OpenAI 兼容客户端 // Logan.Li
func NewClient(cfg Config) Client {
timeout := cfg.Timeout
if timeout <= 0 {
timeout = 180 * time.Second
}
return &OpenAIClient{
cfg: cfg,
client: &http.Client{Timeout: timeout},
}
}
// Name 返回客户端名称 // Logan.Li
func (c *OpenAIClient) Name() string { return "openai-compatible(" + c.cfg.Model + ")" }
// openaiRequest OpenAI chat/completions 请求体 // Logan.Li
type openaiRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Tools []Tool `json:"tools,omitempty"`
ToolChoice string `json:"tool_choice,omitempty"` // "auto" 让模型自主决定
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
}
// openaiResponse OpenAI chat/completions 响应体 // Logan.Li
type openaiResponse struct {
Choices []struct {
Message struct {
Role string `json:"role"`
Content string `json:"content"`
ToolCalls []ToolCall `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
Usage Usage `json:"usage"`
Error *struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error,omitempty"`
}
// Complete 调用 OpenAI 兼容接口完成一轮补全 // Logan.Li
// 返回 assistant 消息(含思考文本 content 与工具调用计划 tool_calls)+ token 统计。
func (c *OpenAIClient) Complete(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
payload := openaiRequest{
Model: c.cfg.Model,
Messages: req.Messages,
Tools: req.Tools,
ToolChoice: "auto",
Temperature: c.cfg.Temperature,
MaxTokens: c.cfg.MaxTokens,
}
if payload.MaxTokens <= 0 {
payload.MaxTokens = 4096
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("序列化请求失败: %w", err)
}
url := c.cfg.Endpoint + "/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("构造请求失败: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+c.cfg.APIKey)
resp, err := c.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("LLM 连接失败: %w", err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("LLM 返回错误 (HTTP %d): %s", resp.StatusCode, extractErrorMessage(raw))
}
var parsed openaiResponse
if err := json.Unmarshal(raw, &parsed); err != nil {
return nil, fmt.Errorf("解析 LLM 响应失败: %w", err)
}
if parsed.Error != nil && parsed.Error.Message != "" {
return nil, fmt.Errorf("LLM 错误: %s", parsed.Error.Message)
}
if len(parsed.Choices) == 0 {
return nil, fmt.Errorf("LLM 未返回任何结果")
}
choice := parsed.Choices[0].Message
return &CompletionResponse{
Model: c.cfg.Model,
Message: Message{
Role: choice.Role,
Content: choice.Content,
ToolCalls: choice.ToolCalls,
},
Usage: parsed.Usage,
}, nil
}
// extractErrorMessage 从上游错误响应提取 message 字段 // Logan.Li
func extractErrorMessage(raw []byte) string {
var m struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
if json.Unmarshal(raw, &m) == nil && m.Error.Message != "" {
return m.Error.Message
}
return string(raw)
}
+73
View File
@@ -0,0 +1,73 @@
// Package llm 提供 Agent CLI 引擎使用的 LLM 抽象层。 // Logan.Li
// 仅保留与 ReAct 循环相关的最小接口:消息、工具调用、补全请求/响应、Client。
// 设计参考 opsxcli/internal/llm,剥离 CLI 配置向导与多 Provider 文件配置,
// 配置由上层(controller)从 model.LLMConfig 解析后注入。
package llm
import "context"
// Message 聊天消息(OpenAI 兼容格式) // Logan.Li
type Message struct {
Role string `json:"role"` // system / user / assistant / tool
Content string `json:"content"` // 消息内容
Name string `json:"name,omitempty"` // tool 消息对应的工具名
ToolCallID string `json:"tool_call_id,omitempty"` // tool 消息对应的 tool_call id
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // assistant 消息携带的工具调用
}
// ToolCall 工具调用 // Logan.Li
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"` // 固定 "function"
Function FunctionCall `json:"function"`
}
// FunctionCall 函数调用(名称 + JSON 字符串参数) // Logan.Li
type FunctionCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"` // JSON 字符串
}
// Tool LLM 工具定义(function calling) // Logan.Li
type Tool struct {
Type string `json:"type"` // 固定 "function"
Function ToolFunction `json:"function"`
}
// ToolFunction 工具函数定义(给 LLM 看的 name/description/parameters) // Logan.Li
type ToolFunction struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"` // JSON Schema
}
// CompletionRequest 补全请求参数 // Logan.Li
type CompletionRequest struct {
Messages []Message `json:"messages"`
Tools []Tool `json:"tools,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
}
// CompletionResponse 补全响应(取首个 choice) // Logan.Li
type CompletionResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Message Message `json:"message"`
Usage Usage `json:"usage"`
}
// Usage token 使用统计 // Logan.Li
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
// Client LLM 客户端接口 // Logan.Li
type Client interface {
// Complete 一次性补全(返回思考文本 + 工具调用计划)。
Complete(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error)
// Name 返回客户端名称(用于日志/审计)。
Name() string
}