- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏 - 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复 - 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示 - 审计历史页只读(移除删除),移除回放弹框录制搜索
262 lines
6.8 KiB
Go
262 lines
6.8 KiB
Go
// 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
|
||
}
|
||
|
||
// FindSkills 按任务类型查找程序记忆(运维流程,最多 n 条) // Logan.Li
|
||
func (m *Memories) FindSkills(taskType string, n int) []SkillEntry {
|
||
all := readJSONL[SkillEntry](m.sklPath)
|
||
var matched []SkillEntry
|
||
for _, s := range all {
|
||
if taskType == "" || s.When == taskType {
|
||
matched = append(matched, s)
|
||
}
|
||
}
|
||
if len(matched) > n {
|
||
matched = matched[len(matched)-n:]
|
||
}
|
||
return matched
|
||
}
|
||
|
||
// 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),
|
||
}
|
||
}
|