Files
Logan 1acb0c49af feat: Agent CLI 与终端审计回放全面优化
- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏
- 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复
- 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示
- 审计历史页只读(移除删除),移除回放弹框录制搜索
2026-07-01 12:13:53 +08:00

308 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// engine.go - 自进化引擎(Evolver)核心 // Logan.Li
// 保留 opsxcli 13 步进化循环的思想,精简为核心步骤:
// OBSERVE → CLASSIFY → LEARN → STORE → MEMORIZE → PREDICT(注入下次 Prompt)。
// 异步执行、失败优雅降级(LLM 不可用退化为规则生成),绝不影响主 ReAct 循环。
package evolver
import (
"context"
"fmt"
"strings"
"time"
"ezops/pkg/agentcli/llm"
)
// ToolCallRecord 工具调用记录(供 Evolver 观察学习) // Logan.Li
type ToolCallRecord struct {
ToolName string `json:"tool_name"`
Args map[string]interface{} `json:"args"`
Output string `json:"output"`
Duration time.Duration `json:"duration"`
Success bool `json:"success"`
RiskLevel int `json:"risk_level"`
}
// TaskExecution 一次任务的完整执行记录 // Logan.Li
type TaskExecution struct {
Query string `json:"query"`
ToolCalls []ToolCallRecord `json:"tool_calls"`
TotalSteps int `json:"total_steps"`
TotalTokens int `json:"total_tokens"`
Duration time.Duration `json:"duration"`
Success bool `json:"success"`
FinalAnswer string `json:"final_answer"`
Timestamp time.Time `json:"timestamp"`
ServerHost string `json:"server_host"` // 用于环境记忆
}
// EvolveResult 进化结果(会话级回灌 Prompt) // Logan.Li
type EvolveResult struct {
TaskType string `json:"task_type"`
LearnedHint string `json:"learned_hint"`
ToolSequence []string `json:"tool_sequence"`
ExperienceAdded bool `json:"experience_added"`
SkillAdded bool `json:"skill_added"`
Consolidated bool `json:"consolidated"`
}
// EvolverEngine 自进化引擎 // Logan.Li
type EvolverEngine struct {
mem *Memories
llm llm.Client // 可选,用于提炼经验/事实
memDir string
}
// NewEvolverEngine 创建进化引擎(llm 可为 nil,退化为规则学习) // Logan.Li
func NewEvolverEngine(memDir string, client llm.Client) (*EvolverEngine, error) {
if memDir == "" {
memDir = "data/agentcli"
}
if err := ensureDir(memDir); err != nil {
return nil, fmt.Errorf("初始化记忆目录失败: %w", err)
}
return &EvolverEngine{
mem: NewMemories(memDir),
llm: client,
memDir: memDir,
}, nil
}
// Evolve 执行一次进化(观察任务 → 学习经验 → 存储) // Logan.Li
// 设计为幂等、可降级:任何子步骤失败都不返回 error 给调用方(仅影响学习质量)。
func (e *EvolverEngine) Evolve(ctx context.Context, exec *TaskExecution) *EvolveResult {
if exec == nil {
return nil
}
result := &EvolveResult{}
// Step 1: OBSERVE —— 提取工具序列
for _, tc := range exec.ToolCalls {
result.ToolSequence = append(result.ToolSequence, tc.ToolName)
}
// Step 2: CLASSIFY —— 任务类型分类(关键词规则,无需 LLM)
result.TaskType = classifyTask(exec.Query, exec.ToolCalls)
// Step 3: LEARN —— 生成经验 hint(优先 LLM,降级规则)
result.LearnedHint = e.learnHint(ctx, exec, result.TaskType)
// Step 4: STORE —— 存入经验记忆 + 环境记忆(主机)
exp := ExperienceEntry{
TaskType: result.TaskType,
Query: truncate(exec.Query, 200),
Hint: result.LearnedHint,
ToolSequence: result.ToolSequence,
Success: exec.Success,
Timestamp: exec.Timestamp.Unix(),
}
if err := e.mem.AddExperience(exp); err == nil {
result.ExperienceAdded = true
}
if exec.ServerHost != "" {
_ = e.mem.AddHost(exec.ServerHost) // 失败忽略
}
// Step 5: MEMORIZE —— LLM 提炼持久事实(可选,失败降级)
if e.llm != nil && exec.Success && exec.FinalAnswer != "" {
go e.memorizeFact(ctx, exec) // 异步,不阻塞进化主流程
}
// Step 6: PROCEDURAL —— 成功任务沉淀程序记忆(工具命令序列) // Logan.Li
if exec.Success {
if steps := extractSSHSteps(exec.ToolCalls); len(steps) >= 2 {
skill := SkillEntry{
Name: result.TaskType + "标准流程",
When: result.TaskType,
Steps: steps,
Timestamp: time.Now().Unix(),
}
if err := e.mem.AddSkill(skill); err == nil {
result.SkillAdded = true
}
}
}
return result
}
// learnHint 生成经验提示(LLM 优先,降级规则) // Logan.Li
func (e *EvolverEngine) learnHint(ctx context.Context, exec *TaskExecution, taskType string) string {
tools := strings.Join(uniqueStrings(resultToolNames(exec.ToolCalls)), " → ")
// 尝试用 LLM 提炼
if e.llm != nil {
if hint := e.llmLearnHint(ctx, exec, taskType, tools, exec.Success); hint != "" {
return hint
}
}
// 降级:规则生成
if exec.Success {
return fmt.Sprintf("完成「%s」类任务时,工具序列 %s 最终成功,可作为参考。", taskType, tools)
}
return fmt.Sprintf("尝试「%s」类任务时,工具序列 %s 未成功,下次可换用不同命令或思路。", taskType, tools)
}
// llmLearnHint 调用 LLM 提炼一句经验 // Logan.Li
func (e *EvolverEngine) llmLearnHint(ctx context.Context, exec *TaskExecution, taskType, tools string, success bool) string {
result := "失败"
if success {
result = "成功"
}
prompt := fmt.Sprintf(
"你是运维经验提炼器。根据以下任务执行记录,用一句中文(不超过80字)提炼经验提示,"+
"说明下次做同类任务的关键要点或推荐工具顺序。只返回经验文本,不要多余解释。\n"+
"任务: %s\n工具序列: %s\n结果: %s",
truncate(exec.Query, 150), tools, result)
resp, err := e.llm.Complete(ctx, &llm.CompletionRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil || resp == nil {
return ""
}
return strings.TrimSpace(truncate(resp.Message.Content, 120))
}
// memorizeFact 异步提取持久事实存入事实记忆 // Logan.Li
func (e *EvolverEngine) memorizeFact(ctx context.Context, exec *TaskExecution) {
defer func() { _ = recover() }()
prompt := fmt.Sprintf(
"从以下运维任务记录中,提炼 1 条值得长期记住的事实(如某主机的固定配置、已知问题、约定)。"+
"如果没有明显可沉淀的事实,回复「无」。只返回事实本身,一句话。\n任务: %s\n结论: %s",
truncate(exec.Query, 150), truncate(exec.FinalAnswer, 300))
ctx2, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
resp, err := e.llm.Complete(ctx2, &llm.CompletionRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil || resp == nil {
return
}
fact := strings.TrimSpace(resp.Message.Content)
if fact == "" || strings.HasPrefix(fact, "无") {
return
}
_ = e.mem.AddFact(FactEntry{Fact: truncate(fact, 150), Source: truncate(exec.Query, 80)})
}
// GetContextForPrompt 为下一次任务构建记忆上下文(注入 system prompt) // Logan.Li
func (e *EvolverEngine) GetContextForPrompt(query string) string {
taskType := classifyTask(query, nil)
var sb strings.Builder
if exps := e.mem.FindExperiences(taskType, 3); len(exps) > 0 {
sb.WriteString(fmt.Sprintf(" 【%s 类历史经验】\n", taskType))
for _, exp := range exps {
mark := "成功"
if !exp.Success {
mark = "未成功"
}
sb.WriteString(fmt.Sprintf(" - (%s) %s\n", mark, exp.Hint))
}
}
if facts := e.mem.RelevantFacts(query, 3); len(facts) > 0 {
sb.WriteString(" 【已知事实】\n")
for _, f := range facts {
sb.WriteString(fmt.Sprintf(" - %s\n", f.Fact))
}
}
if hosts := e.mem.Hosts(); len(hosts) > 0 {
sb.WriteString(fmt.Sprintf(" 【已知主机】%s\n", strings.Join(hosts, ", ")))
}
if skills := e.mem.FindSkills(taskType, 2); len(skills) > 0 {
sb.WriteString(" 【推荐流程】\n")
for _, sk := range skills {
sb.WriteString(fmt.Sprintf(" - %s: %s\n", sk.Name, strings.Join(sk.Steps, " → ")))
}
}
return sb.String()
}
// GetStats 进化统计(供审计/展示) // Logan.Li
func (e *EvolverEngine) GetStats() map[string]interface{} {
return map[string]interface{}{
"enabled": true,
"memory": e.mem.CountStats(),
}
}
// ---- 任务分类(关键词规则) ---- // Logan.Li
// classifyTask 按 query + 工具调用关键词分类任务类型 // Logan.Li
func classifyTask(query string, calls []ToolCallRecord) string {
text := strings.ToLower(query)
for _, c := range calls {
if cmd, ok := c.Args["command"].(string); ok {
text += " " + strings.ToLower(cmd)
}
}
rules := []struct {
typ string
kws []string
}{
{"磁盘空间", []string{"disk", "空间", "df", "du", "inode", "磁盘", "满了"}},
{"网络", []string{"network", "网络", "ping", "netstat", "ss ", "端口", "连接", "tcp", "dns"}},
{"服务管理", []string{"service", "服务", "systemctl", "nginx", "mysql", "redis", "restart", "启动", "停止"}},
{"软件安装", []string{"install", "安装", "yum", "apt", "docker", "部署", "deploy", "pip", "npm"}},
{"进程排查", []string{"process", "进程", "ps ", "kill", "cpu", "top", "卡死", "hang"}},
{"内存排查", []string{"memory", "内存", "mem", "free", "oom", "swap"}},
{"日志排查", []string{"log", "日志", "journalctl", "tail", "grep"}},
{"权限问题", []string{"permission", "权限", "chmod", "chown", "denied", "拒绝", "forbidden"}},
{"性能调优", []string{"performance", "性能", "负载", "load", "慢", "slow", "latency", "延迟"}},
{"配置管理", []string{"config", "配置", "conf", "vim", "sed"}},
}
for _, r := range rules {
for _, kw := range r.kws {
if strings.Contains(text, kw) {
return r.typ
}
}
}
return "通用运维"
}
// ---- 辅助 ---- // Logan.Li
func truncate(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "..."
}
func resultToolNames(calls []ToolCallRecord) []string {
out := make([]string, 0, len(calls))
for _, c := range calls {
out = append(out, c.ToolName)
}
return out
}
// extractSSHSteps 从工具调用记录提取 ssh_execute 命令序列 // Logan.Li
func extractSSHSteps(calls []ToolCallRecord) []string {
out := make([]string, 0, len(calls))
for _, c := range calls {
if c.ToolName != "ssh_execute" {
continue
}
if cmd, ok := c.Args["command"].(string); ok && strings.TrimSpace(cmd) != "" {
out = append(out, strings.TrimSpace(cmd))
}
}
return out
}
func uniqueStrings(in []string) []string {
seen := make(map[string]bool)
out := make([]string, 0, len(in))
for _, s := range in {
if !seen[s] {
seen[s] = true
out = append(out, s)
}
}
return out
}