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,271 @@
|
||||
// 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"`
|
||||
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) // 异步,不阻塞进化主流程
|
||||
}
|
||||
|
||||
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, ", ")))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user