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:
1 parent
f2069fe3f0
commit
92a2247390
37 files changed
+6449
-6
No files matched your search
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user