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

+246
View File
@@ -0,0 +1,246 @@
// Package evolver 实现 Agent CLI 的自进化引擎(Evolver)。 // Logan.Li
// 设计参考 opsxcli internal/agent/evolver,保留四层记忆 + 进化循环思想,精简适配为后端库:
// - 记忆持久化用文件(data/agentcli/*.jsonl),与 opsxcli 原生一致,零 DB 依赖。
// - 进化循环异步执行,失败优雅降级,绝不影响主 ReAct 循环。
// - LLM 可用时用于提炼经验/事实;不可用时退化为规则生成。
package evolver
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
// ---- 基础文件存储 ---- // Logan.Li
// ensureDir 确保目录存在 // Logan.Li
func ensureDir(dir string) error { return os.MkdirAll(dir, 0o755) }
// readJSONL 读取 JSONL 文件为切片 // Logan.Li
func readJSONL[T any](path string) []T {
raw, err := os.ReadFile(path)
if err != nil || len(raw) == 0 {
return nil
}
var out []T
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
var item T
if json.Unmarshal([]byte(line), &item) == nil {
out = append(out, item)
}
}
return out
}
// appendJSONL 追加一行 JSON // Logan.Li
func appendJSONL[T any](path string, item T) error {
b, err := json.Marshal(item)
if err != nil {
return err
}
b = append(b, '\n')
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(b)
return err
}
// readJSON 读取整个 JSON 文件(失败返回默认值) // Logan.Li
func readJSON[T any](path string, def T) T {
raw, err := os.ReadFile(path)
if err != nil {
return def
}
var v T
if json.Unmarshal(raw, &v) != nil {
return def
}
return v
}
// writeJSON 写入整个 JSON 文件 // Logan.Li
func writeJSON[T any](path string, v T) error {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, b, 0o644)
}
// ---- 四层记忆 ---- // Logan.Li
// ExperienceEntry 经验记忆条目(最核心,直接注入下次 Prompt) // Logan.Li
type ExperienceEntry struct {
TaskType string `json:"task_type"`
Query string `json:"query"`
Hint string `json:"hint"` // 经验提示
ToolSequence []string `json:"tool_sequence"` // 推荐工具序列
Success bool `json:"success"`
Timestamp int64 `json:"timestamp"`
}
// FactEntry 事实记忆条目(持久事实) // Logan.Li
type FactEntry struct {
Fact string `json:"fact"`
Source string `json:"source"`
Timestamp int64 `json:"timestamp"`
}
// SkillEntry 程序记忆条目(运维技能/流程) // Logan.Li
type SkillEntry struct {
Name string `json:"name"`
When string `json:"when"`
Steps []string `json:"steps"`
Timestamp int64 `json:"timestamp"`
}
// Memories 四层记忆集合(线程安全) // Logan.Li
type Memories struct {
dir string
mu sync.Mutex
expPath string // experience.jsonl
facPath string // factual.jsonl
sklPath string // procedural.jsonl
envPath string // environment.json
}
// NewMemories 创建四层记忆(dir 为 data/agentcli) // Logan.Li
func NewMemories(dir string) *Memories {
return &Memories{
dir: dir,
expPath: filepath.Join(dir, "experience.jsonl"),
facPath: filepath.Join(dir, "factual.jsonl"),
sklPath: filepath.Join(dir, "procedural.jsonl"),
envPath: filepath.Join(dir, "environment.json"),
}
}
// AddExperience 追加经验 // Logan.Li
func (m *Memories) AddExperience(e ExperienceEntry) error {
m.mu.Lock()
defer m.mu.Unlock()
if e.Timestamp == 0 {
e.Timestamp = time.Now().Unix()
}
return appendJSONL(m.expPath, e)
}
// FindExperiences 按 task_type 查找最近的经验(最多 n 条,优先成功) // Logan.Li
func (m *Memories) FindExperiences(taskType string, n int) []ExperienceEntry {
all := readJSONL[ExperienceEntry](m.expPath)
var matched []ExperienceEntry
for _, e := range all {
if taskType == "" || e.TaskType == taskType {
matched = append(matched, e)
}
}
// 优先成功经验,按时间倒序取前 n
successFirst := make([]ExperienceEntry, 0, len(matched))
others := make([]ExperienceEntry, 0, len(matched))
for _, e := range matched {
if e.Success {
successFirst = append(successFirst, e)
} else {
others = append(others, e)
}
}
// 简单逆序(文件本身就是时间顺序,取末尾最新)
pick := func(s []ExperienceEntry) []ExperienceEntry {
if len(s) <= n {
return s
}
return s[len(s)-n:]
}
out := append(pick(successFirst), pick(others)...)
if len(out) > n {
out = out[len(out)-n:]
}
return out
}
// AddFact 追加事实 // Logan.Li
func (m *Memories) AddFact(f FactEntry) error {
m.mu.Lock()
defer m.mu.Unlock()
if f.Timestamp == 0 {
f.Timestamp = time.Now().Unix()
}
return appendJSONL(m.facPath, f)
}
// RelevantFacts 返回与 query 关键词相关的事实(最多 n 条) // Logan.Li
func (m *Memories) RelevantFacts(query string, n int) []FactEntry {
all := readJSONL[FactEntry](m.facPath)
if len(all) == 0 {
return nil
}
kw := strings.ToLower(query)
var out []FactEntry
for _, f := range all {
if strings.Contains(strings.ToLower(f.Fact), kw) || strings.Contains(kw, strings.ToLower(f.Fact)) {
out = append(out, f)
}
}
if len(out) > n {
out = out[len(out)-n:]
}
return out
}
// AddSkill 追加技能 // Logan.Li
func (m *Memories) AddSkill(s SkillEntry) error {
m.mu.Lock()
defer m.mu.Unlock()
if s.Timestamp == 0 {
s.Timestamp = time.Now().Unix()
}
return appendJSONL(m.sklPath, s)
}
// KnownHosts 已知主机(环境记忆) // Logan.Li
type KnownHosts struct {
Hosts []string `json:"hosts"`
}
// AddHost 记录已知主机(去重) // Logan.Li
func (m *Memories) AddHost(host string) error {
if host == "" {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
env := readJSON(m.envPath, KnownHosts{})
for _, h := range env.Hosts {
if h == host {
return nil
}
}
env.Hosts = append(env.Hosts, host)
return writeJSON(m.envPath, env)
}
// Hosts 返回已知主机列表 // Logan.Li
func (m *Memories) Hosts() []string {
return readJSON(m.envPath, KnownHosts{}).Hosts
}
// CountStats 统计各层记忆条目数 // Logan.Li
func (m *Memories) CountStats() map[string]int {
return map[string]int{
"experience": len(readJSONL[ExperienceEntry](m.expPath)),
"factual": len(readJSONL[FactEntry](m.facPath)),
"skill": len(readJSONL[SkillEntry](m.sklPath)),
"hosts": len(readJSON(m.envPath, KnownHosts{}).Hosts),
}
}