From 5d9498f68771313f754ae46050afb302376266d4 Mon Sep 17 00:00:00 2001 From: titor Date: Mon, 27 Apr 2026 06:16:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=B0=E5=BF=86=E4=BD=93=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=20v0.3.0=20=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 核心功能 - 双记忆系统合并:picoclaw MEMORY.md + hxclaw 会话摘要 - 独立上下文系统:不依赖 picoclaw session - 向量检索:硅基流动 BGE-M3 API - 三重检测:关键词/向量相似度/命令 ## 数据库 - libSQL (TursoDB) 存储 - sessions + chats 表设计 - 向量存储使用 binary 编码 ## 查询场景 - RecallHistory: 查询所有会话摘要 - RecallTopic: 按话题向量检索 - RecallSession: 指定会话详情 - RecallWithinSession: 会话内检索 ## 导出 - MongoDB 风格:~/.config/hxclaw/export-data.json - chats 嵌套在 sessions 下 - 增量导出,同 session 累加 ## UI 优化 - 合并状态显示(耗时 · 状态 · 消息数) - 颜色设计:金色图标 + 暗绿色/暗红色状态 ## 配置项 - memory.recall: keywords, auto_recall, similarity_threshold - memory.vector: max_search_results - memory.auto_export --- agents.md | 51 +++- changelog.md | 76 +++++- cmd/hxclaw/internal/config.go | 195 ++++++++++++-- cmd/hxclaw/main.go | 196 ++++++++++++++- docs/architecture.md | 461 ++++++++++++++++++++++++++++++++++ docs/memory-plan.md | 260 +++++++++++++++++++ docs/picoclaw-research.md | 238 ++++++++++++++++++ go.mod | 13 +- go.sum | 16 +- go_modules/libsql-client-go | 1 + project.config.yml | 25 +- taolun.md | 103 +++++++- 12 files changed, 1583 insertions(+), 52 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/memory-plan.md create mode 100644 docs/picoclaw-research.md create mode 160000 go_modules/libsql-client-go diff --git a/agents.md b/agents.md index 73dca7d..2bb4a77 100644 --- a/agents.md +++ b/agents.md @@ -41,14 +41,13 @@ ## 当前任务 -### v0.2.0 目标 +### v0.3.0 目标 -实现 TTS 语音朗读功能: -1. 集成 mimo-tts client(TCP 连接本地 daemon) -2. 添加配置文件 TTS 开关 -3. 实现命令行切换(/tts on/off/status) -4. 实现临时 TTS 前缀(`T 消息`) -5. 动态提示符显示状态(👀 🔊) +实现聊天记忆体功能: +1. 集成 libSQL (TursoDB) 数据库 +2. 实现会话维度的短期记忆 +3. 支持向量检索(硅基流动 API) +4. 命令行支持(/new, /memory, /sessions) --- @@ -81,6 +80,44 @@ --- +### v0.3.0 进度 + +1. **数据库层** + - 集成 libSQL (TursoDB) + - 创建 sessions 和 chats 表 + - 实现 CRUD 操作 + - 数据库保存在 `~/.config/hxclaw/hxclaw.db` + +2. **独立上下文系统** + - 创建 GetContextPrompt() 返回会话摘要 + - 注入到 ProcessDirect() 调用前 + - 不再依赖 picoclaw session + +3. **向量服务** + - 封装硅基流动 Embedding API + - 实现向量生成和相似度检索 + +4. **会话管理** + - 自动创建 Session(首次输入时) + - 手动创建(/new 命令) + - 消息摘要生成 + +5. **UI 优化** + - 合并状态显示到单行 + - 金色图标 + 灰色文字 + - 暗绿色"会话已保存"/暗红色"保存异常" + +6. **双记忆系统合并** + - 读取 picoclaw 的 MEMORY.md 作为长期记忆 + - 合并到 hxclaw 的会话摘要上下文 + - AI 同时看到长期记忆和会话摘要 + +5. **JSON 导出** + - 退出时自动导出 + - 手动导出 + +--- + ## 项目配置 ### project.config.yml diff --git a/changelog.md b/changelog.md index 749b21c..890c7f5 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,58 @@ ## 版本记录 +### v0.3.0 (2026-04-27) + +- **双记忆系统合并** + - 读取 picoclaw 的 MEMORY.md 作为长期记忆 + - 合并到 hxclaw 的会话摘要上下文 + - AI 同时看到长期记忆和会话摘要 + +- **独立上下文系统** + - 创建 GetContextPrompt() 返回会话摘要 + - 注入到 ProcessDirect() 调用前 + - 不再依赖 picoclaw session 管理 + - 修复 recall 结果污染 session summary 问题 + +- **数据库层完善** + - 集成 libSQL (TursoDB) + - 创建 sessions 和 chats 表 + - 实现 CRUD 操作 + - 数据库保存在 `~/.config/hxclaw/hxclaw.db` + - 向量存储使用 binary 编码(float32) + +- **向量检索功能** + - 硅基流动 BGE-M3 API 集成 + - 向量生成和存储 + - Cosine Similarity 计算 + - SearchSimilar() 函数实现 + - 4 个查询场景(RecallHistory, RecallTopic, RecallSession, RecallWithinSession) + +- **三重检测机制** + - 关键词匹配(之前、聊过、记得等) + - 向量相似度自动检测(auto_recall + 阈值) + - /recall 命令强制触发 + - 配置项:keywords, auto_recall, similarity_threshold, max_results + +- **MongoDB 风格导出** + - 固定路径:`~/.config/hxclaw/export-data.json` + - chats 嵌套在 sessions 下 + - 增量导出,同 session 累加 + - 版本控制(version 字段) + +- **UI 优化** + - 合并状态显示到单行(耗时 · 状态 · 消息数) + - 颜色设计:金色图标 + 灰色文字 + - 暗绿色"会话已保存" / 暗红色"会话保存异常" + +- **配置项更新** + - memory.recall 配置 + - memory.vector.max_search_results + - memory.auto_export(替换 export_on_exit) + - 默认 max_search_results = 10 + +--- + ### v0.2.1 - 修复 TTS JSON 请求格式,兼容 Windows daemon @@ -42,11 +94,28 @@ - [x] 动态提示符显示状态 - [x] 静默失败处理 -### v0.3.0 (计划) +### v0.3.0 (当前) + +- [x] 双记忆系统合并(picoclaw MEMORY.md + hxclaw 会话摘要) +- [x] 数据库层集成(libSQL) +- [x] 独立上下文系统(不再依赖 picoclaw session) +- [x] 会话摘要注入 +- [x] UI 优化(合并显示、颜色设计) +- [x] 向量检索(硅基流动 API) +- [x] 4 个查询场景(RecallHistory, RecallTopic...) +- [x] 三重检测机制 +- [x] MongoDB 风格导出 + +--- + +## 待实现功能 + +### v0.4.0 (计划) - [ ] 命令行参数支持(--theme, --tts 等) - [ ] 多语言支持 -- [ ] 会话历史持久化 +- [ ] /new 命令开始新会话 +- [ ] /memory list|show 命令 --- @@ -64,6 +133,9 @@ - [x] 实现 Markdown 渲染(glamour) - [x] 实现项目配置化(project.config.yml) - [x] 实现 TTS 语音朗读功能 +- [x] 集成 libSQL 数据库 +- [x] 实现独立上下文系统 +- [x] UI 状态合并显示 --- diff --git a/cmd/hxclaw/internal/config.go b/cmd/hxclaw/internal/config.go index c7a8b32..a405954 100644 --- a/cmd/hxclaw/internal/config.go +++ b/cmd/hxclaw/internal/config.go @@ -20,6 +20,8 @@ type Config struct { UI UIConfig `yaml:"ui"` // TTS TTS 语音配置 TTS TTSConfig `yaml:"tts"` + // Memory 聊天记忆体配置 + Memory MemoryConfig `yaml:"memory"` } // StreamingConfig 流式输出配置,控制模拟打字效果的延迟时间 @@ -56,6 +58,48 @@ type TTSConfig struct { Auto bool `yaml:"auto"` } +// MemoryConfig 聊天记忆体配置 +type MemoryConfig struct { + // Enabled 启用开关 + Enabled bool `yaml:"enabled"` + // DBPath 数据库路径 + DBPath string `yaml:"db_path"` + // AutoSession 自动创建 Session + AutoSession bool `yaml:"auto_session"` + // AutoExport 退出时自动导出 + AutoExport bool `yaml:"auto_export"` + // Vector 向量服务配置 + Vector VectorConfig `yaml:"vector"` + // Recall 检索配置 + Recall RecallConfig `yaml:"recall"` +} + +// VectorConfig 向量服务配置 +type VectorConfig struct { + // APIKey 硅基流动 API Key + APIKey string `yaml:"api_key"` + // BaseURL API 地址 + BaseURL string `yaml:"base_url"` + // Model 向量模型 + Model string `yaml:"model"` + // Dimension ��量维度 + Dimension int `yaml:"dimension"` + // MaxSearchResults 最大检索结果数 + MaxSearchResults int `yaml:"max_search_results"` +} + +// RecallConfig 检索配置 +type RecallConfig struct { + // Keywords 触发关键词列表 + Keywords []string `yaml:"keywords"` + // AutoRecall 是否自动检测相似度 + AutoRecall bool `yaml:"auto_recall"` + // SimilarityThreshold 相似度阈值 + SimilarityThreshold float64 `yaml:"similarity_threshold"` + // MaxResults 最大检索结果数 + MaxResults int `yaml:"max_results"` +} + var ( // defaultCfg 默认配置值,当配置文件不存在或字段为空时使用 defaultCfg = Config{ @@ -76,6 +120,25 @@ var ( Port: 9876, Auto: true, }, +Memory: MemoryConfig{ + Enabled: true, + DBPath: "", + AutoSession: true, + AutoExport: true, + Vector: VectorConfig{ + APIKey: "", + BaseURL: "https://api.siliconflow.cn/v1", + Model: "BAAI/bge-m3", + Dimension: 1024, + MaxSearchResults: 10, + }, + Recall: RecallConfig{ + Keywords: []string{"之前", "聊过", "记得", "找找", "曾经", "谈论过", "提过"}, + AutoRecall: true, + SimilarityThreshold: 0.7, + MaxResults: 5, + }, + }, } cfg *Config // 已合并的配置(用户配置 + 项目配置) cfgLock sync.RWMutex // 配置读写锁 @@ -83,8 +146,7 @@ var ( // 用户配置文件路径常量 const ( - userConfigDir = ".config/hxclaw" // 用户配置目录(相对于用户家目录) - userConfigFile = "config.yml" // 用户配置文件名 + userConfigFile = "config.yml" // 用户配置文件名 ) // LoadProjectConfig 加载并合并项目配置 @@ -142,8 +204,8 @@ func createUserConfig(userPath string) error { # Markdown 渲染配置 markdown: - theme: dark # 渲染主题:dark, light, dracula, tokyo-night 等 - line_width: 0 # 自动换行宽度(-1=禁用,0=自动,>0=固定宽度) + theme: dark + line_width: 0 # UI 配置 ui: @@ -152,8 +214,25 @@ ui: # TTS 语音配置 tts: - enabled: false # 全局开关(默认关闭) - auto: true # AI 回复后自动朗读 + enabled: false + auto: true + +# 聊天记忆体配置 +memory: + enabled: true + auto_session: true + auto_export: true + vector: + api_key: "" + base_url: "https://api.siliconflow.cn/v1" + model: "BAAI/bge-m3" + dimension: 1024 + max_search_results: 10 + recall: + keywords: ["之前", "聊过", "记得", "找找", "曾经", "谈论过", "提过"] + auto_recall: true + similarity_threshold: 0.7 + max_results: 5 ` return os.WriteFile(userPath, []byte(defaultContent), 0644) } @@ -161,7 +240,7 @@ tts: // loadProjectConfig 加载项目级配置文件 // 路径优先级:环境变量 HXCLAW_CONFIG 指定路径 > ./project.config.yml func loadProjectConfig() *Config { - cfgPath := getProjectConfigPath() + cfgPath := GetProjectConfigPath() if cfgPath == "" { return &defaultCfg } @@ -210,6 +289,47 @@ func mergeConfig(userCfg, projCfg *Config) *Config { if projCfg.TTS.Port > 0 { result.TTS.Port = projCfg.TTS.Port } + // Memory 配置 + if projCfg.Memory.Enabled { + result.Memory.Enabled = projCfg.Memory.Enabled + } + if projCfg.Memory.DBPath != "" { + result.Memory.DBPath = projCfg.Memory.DBPath + } + if projCfg.Memory.AutoSession { + result.Memory.AutoSession = projCfg.Memory.AutoSession + } + if projCfg.Memory.AutoExport { + result.Memory.AutoExport = projCfg.Memory.AutoExport + } + if projCfg.Memory.Vector.APIKey != "" { + result.Memory.Vector.APIKey = projCfg.Memory.Vector.APIKey + } + if projCfg.Memory.Vector.BaseURL != "" { + result.Memory.Vector.BaseURL = projCfg.Memory.Vector.BaseURL + } + if projCfg.Memory.Vector.Model != "" { + result.Memory.Vector.Model = projCfg.Memory.Vector.Model + } + if projCfg.Memory.Vector.Dimension > 0 { + result.Memory.Vector.Dimension = projCfg.Memory.Vector.Dimension + } + if projCfg.Memory.Vector.MaxSearchResults > 0 { + result.Memory.Vector.MaxSearchResults = projCfg.Memory.Vector.MaxSearchResults + } + // Recall 配置 + if len(projCfg.Memory.Recall.Keywords) > 0 { + result.Memory.Recall.Keywords = projCfg.Memory.Recall.Keywords + } + if projCfg.Memory.Recall.AutoRecall { + result.Memory.Recall.AutoRecall = projCfg.Memory.Recall.AutoRecall + } + if projCfg.Memory.Recall.SimilarityThreshold > 0 { + result.Memory.Recall.SimilarityThreshold = projCfg.Memory.Recall.SimilarityThreshold + } + if projCfg.Memory.Recall.MaxResults > 0 { + result.Memory.Recall.MaxResults = projCfg.Memory.Recall.MaxResults + } } // 再应用用户配置(覆盖项目配置) @@ -238,6 +358,48 @@ func mergeConfig(userCfg, projCfg *Config) *Config { if userCfg.TTS.Auto { result.TTS.Auto = userCfg.TTS.Auto } + // Memory 配置(用户配置优先) + if userCfg.Memory.Enabled { + result.Memory.Enabled = userCfg.Memory.Enabled + } + if userCfg.Memory.DBPath != "" { + result.Memory.DBPath = userCfg.Memory.DBPath + } + if userCfg.Memory.AutoSession { + result.Memory.AutoSession = userCfg.Memory.AutoSession + } + if userCfg.Memory.AutoExport { + result.Memory.AutoExport = userCfg.Memory.AutoExport + } + // 向量 API Key 只能在用户配置中指定 + if userCfg.Memory.Vector.APIKey != "" { + result.Memory.Vector.APIKey = userCfg.Memory.Vector.APIKey + } + if userCfg.Memory.Vector.BaseURL != "" { + result.Memory.Vector.BaseURL = userCfg.Memory.Vector.BaseURL + } + if userCfg.Memory.Vector.Model != "" { + result.Memory.Vector.Model = userCfg.Memory.Vector.Model + } + if userCfg.Memory.Vector.Dimension > 0 { + result.Memory.Vector.Dimension = userCfg.Memory.Vector.Dimension + } + if userCfg.Memory.Vector.MaxSearchResults > 0 { + result.Memory.Vector.MaxSearchResults = userCfg.Memory.Vector.MaxSearchResults + } + // Recall 配置 + if len(userCfg.Memory.Recall.Keywords) > 0 { + result.Memory.Recall.Keywords = userCfg.Memory.Recall.Keywords + } + if userCfg.Memory.Recall.AutoRecall { + result.Memory.Recall.AutoRecall = userCfg.Memory.Recall.AutoRecall + } + if userCfg.Memory.Recall.SimilarityThreshold > 0 { + result.Memory.Recall.SimilarityThreshold = userCfg.Memory.Recall.SimilarityThreshold + } + if userCfg.Memory.Recall.MaxResults > 0 { + result.Memory.Recall.MaxResults = userCfg.Memory.Recall.MaxResults + } } return &result @@ -256,21 +418,16 @@ func GetProjectConfig() *Config { // getUserConfigPath 获取用户配置文件路径 // 路径格式:~/.config/hxclaw/config.yml -// 支持 Windows (USERPROFILE) 和 Unix (HOME) 环境变量 func getUserConfigPath() string { - homeDir := os.Getenv("USERPROFILE") - if homeDir == "" { - homeDir = os.Getenv("HOME") - } - if homeDir == "" { - return "" - } - return filepath.Join(homeDir, userConfigDir, userConfigFile) + return GetConfigFile() } -// getProjectConfigPath 获取项目配置文件路径 -// 优先使用环境变量 HXCLAW_CONFIG 指定的路径,否则使用当前目录的 project.config.yml -func getProjectConfigPath() string { +// GetUserConfigDir 获取用户配置目录 +func GetUserConfigDir() string { + return GetConfigDir() +} + +func GetProjectConfigPath() string { if path := os.Getenv("HXCLAW_CONFIG"); path != "" { return path } diff --git a/cmd/hxclaw/main.go b/cmd/hxclaw/main.go index 29f68fa..45786b3 100644 --- a/cmd/hxclaw/main.go +++ b/cmd/hxclaw/main.go @@ -11,6 +11,7 @@ import ( "charm.land/lipgloss/v2" "github.com/hxclaw/hxclaw/cmd/hxclaw/internal" + "github.com/hxclaw/hxclaw/cmd/hxclaw/internal/memory" "github.com/muesli/termenv" "github.com/sipeed/picoclaw/pkg/agent" "github.com/sipeed/picoclaw/pkg/bus" @@ -20,6 +21,8 @@ import ( const Logo = "🦐" +var currentSession *memory.Session + func main() { if err := internal.LoadProjectConfig(); err != nil { fmt.Fprintf(os.Stderr, "错误:加载项目配置失败: %v\n", err) @@ -61,6 +64,26 @@ func main() { "skills_available": startupInfo["skills"].(map[string]any)["available"], }) + memoryCfg := internal.GetProjectConfig().Memory + if memoryCfg.Enabled { + // 优先使用用户配置中的 db_path,如果没有则使用默认路径 + dbPath := memoryCfg.DBPath + if dbPath == "" { + dbPath = memory.GetDefaultDBPath() + } + fmt.Printf("初始化记忆体,db_path: %s\n", dbPath) + if err := memory.Init(memory.WithDBPath(dbPath)); err != nil { + fmt.Fprintf(os.Stderr, "警告:初始化记忆体失败: %v,将使用无记忆模式\n", err) + } else { + fmt.Println("记忆体初始化成功") + memory.InitVector( + memory.WithAPIKey(memoryCfg.Vector.APIKey), + memory.WithBaseURL(memoryCfg.Vector.BaseURL), + memory.WithModel(memoryCfg.Vector.Model), + ) + } + } + fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", Logo) interactiveMode(agentLoop, "cli:default") } @@ -88,6 +111,7 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) { if err != nil { if err == internal.ErrInterrupt || err == internal.ErrEOF { fmt.Println("\n再见!") + memory.ExportIfNeeded() return } fmt.Printf("读取输入错误: %v\n", err) @@ -101,6 +125,7 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) { if input == "exit" || input == "quit" { fmt.Println("再见!") + memory.ExportIfNeeded() return } @@ -116,6 +141,21 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) { continue } + if strings.HasPrefix(input, "/new") { + handleNewSessionCommand(rl, basePrompt) + continue + } + + if strings.HasPrefix(input, "/memory") { + handleMemoryCommand(input) + continue + } + + if strings.HasPrefix(input, "/sessions") { + handleSessionsCommand() + continue + } + if isTempTTS { enabled := internal.ToggleTTS() if enabled { @@ -139,6 +179,7 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { if err != nil { if err == internal.ErrEOF { fmt.Println("\n再见!") + memory.ExportIfNeeded() return } fmt.Printf("读取输入错误: %v\n", err) @@ -152,6 +193,7 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { if input == "exit" || input == "quit" { fmt.Println("再见!") + memory.ExportIfNeeded() return } @@ -167,6 +209,21 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { continue } + if strings.HasPrefix(input, "/new") { + handleNewSessionCommand(nil, internal.GetProjectConfig().UI.UserIcon) + continue + } + + if strings.HasPrefix(input, "/memory") { + handleMemoryCommand(input) + continue + } + + if strings.HasPrefix(input, "/sessions") { + handleSessionsCommand() + continue + } + if isTempTTS { internal.ToggleTTS() } @@ -179,6 +236,18 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string, tempTTS bool) { startTime := time.Now() + // 保存原始输入用于后续保存 + originalInput := input + + // 注入 hxclaw 的上下文摘要 + memoryCfg := internal.GetProjectConfig().Memory + if memoryCfg.Enabled { + contextPrompt := memory.GetContextPrompt(input) + if contextPrompt != "" { + input = contextPrompt + "\n用户新问题: " + input + } + } + spinner := internal.NewSpinner("思考中...") spinner.Start() @@ -187,7 +256,11 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string, temp spinner.Stop() if err != nil { - fmt.Printf("错误: %v\n", err) + fmt.Printf("警告: %v\n", err) + } + + if resp == "" { + fmt.Println("(空响应,跳过保存)") return } @@ -200,8 +273,16 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string, temp go internal.SpeakText(resp) } + // 保存聊天记录到数据库 + var chatCount int + var saveErr error + memoryCfg = internal.GetProjectConfig().Memory + if memoryCfg.Enabled { + chatCount, saveErr = memory.SaveChat(originalInput, resp, !memory.ShouldSkipSummaryUpdate(originalInput)) + } + elapsed := time.Since(startTime) - printElapsed(elapsed) + printElapsed(elapsed, chatCount, saveErr) } func clearSpinnerLine() { @@ -242,17 +323,33 @@ func outputLineByLine(text string) { } var ( - iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e")) - textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32")) + iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e")) + textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32")) + memoryOkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#4a9e6b")) // 暗绿色 + memoryErrStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#c75050")) // 暗红色 ) -func printElapsed(elapsed time.Duration) { +func printElapsed(elapsed time.Duration, chatCount int, saveErr error) { elapsedSec := math.Round(elapsed.Seconds()*10) / 10 elapsedStr := formatDuration(elapsedSec) icon := iconStyle.Render("▣ ") - text := textStyle.Render(fmt.Sprintf("耗时: %s", elapsedStr)) - fmt.Printf(" %s%s\n\n", icon, text) + timeText := textStyle.Render(fmt.Sprintf("耗时: %s", elapsedStr)) + + var statusText string + if saveErr != nil { + statusText = memoryErrStyle.Render("会话保存异常") + } else if chatCount > 0 { + statusText = memoryOkStyle.Render("会话已保存") + } + + memCountText := textStyle.Render(fmt.Sprintf("当前会话 %d 条消息", chatCount)) + + if statusText != "" { + fmt.Printf(" %s%s · %s · %s\n\n", icon, timeText, statusText, memCountText) + } else { + fmt.Printf(" %s%s · %s\n\n", icon, timeText, memCountText) + } } func formatTokens(n int) string { @@ -331,3 +428,88 @@ func handleTTSCommandSimple(input string) { fmt.Println("用法: /tts [on|off|status]") } } + +func handleNewSessionCommand(rl *internal.Readline, basePrompt string) { + uuid, err := memory.CreateNewSession() + if err != nil { + fmt.Printf("创建新会话失败: %v\n", err) + return + } + fmt.Printf("已创建新会话: %s\n", uuid) + currentSession = nil +} + +func handleMemoryCommand(input string) { + args := strings.Fields(input) + if len(args) == 1 || args[1] == "list" { + sessions, err := memory.ListSessions() + if err != nil { + fmt.Printf("查询会话失败: %v\n", err) + return + } + if len(sessions) == 0 { + fmt.Println("暂无会话记录") + return + } + fmt.Printf("共有 %d 个会话记录:\n", len(sessions)) + for _, s := range sessions { + summary := s.Summary + if summary == "" { + summary = "(无摘要)" + } else if len(summary) > 50 { + summary = summary[:50] + "..." + } + fmt.Printf(" - %s: %s\n", s.UUID[:8], summary) + } + return + } + + switch args[1] { + case "show": + session := memory.GetCurrentSession() + if session == nil { + latest, err := memory.GetLatestSession() + if err != nil || latest == nil { + fmt.Println("暂无会话记录") + return + } + session = latest + } + if session.Summary == "" { + fmt.Println("当前会话暂无摘要") + return + } + fmt.Printf("=== 会话摘要 (%s) ===\n%s\n", session.UUID[:8], session.Summary) + case "current": + if currentSession == nil { + fmt.Println("当前无活跃会话") + return + } + fmt.Printf("当前会话: %s\n", currentSession.UUID) + fmt.Printf("聊天记录数: %d\n", len(currentSession.ChatIDs)) + default: + fmt.Println("用法: /memory [list|show|current]") + } +} + +func handleSessionsCommand() { + sessions, err := memory.ListSessions() + if err != nil { + fmt.Printf("查询会话失败: %v\n", err) + return + } + if len(sessions) == 0 { + fmt.Println("暂无会话记录") + return + } + fmt.Printf("共有 %d 个会话记录:\n", len(sessions)) + for _, s := range sessions { + summary := s.Summary + if summary == "" { + summary = "(无摘要)" + } else if len(summary) > 30 { + summary = summary[:30] + "..." + } + fmt.Printf(" %s | %d 条消息 | %s\n", s.UUID[:8], len(s.ChatIDs), summary) + } +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..554b9fa --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,461 @@ +# hxclaw 记忆体系统架构图 + +## 一、数据流向总图 + +```mermaid +flowchart TB + subgraph 用户输入 + A[用户输入] + A1[普通对话] + A2[查询历史] + A3[/recall 命令] + end + + subgraph 上下文注入 + B[GetContextPrompt] + B0[读取 picoclaw MEMORY.md] + B1[获取当前 Session 摘要] + B2[检测查询意图] + B3[按需调用 Recall] + end + + subgraph AI 处理 + C[ProcessDirect] + C1[工具调用] + C2[多轮对话] + end + + subgraph 保存流程 + D[SaveChat] + D1[INSERT chat] + D2[UPDATE chat 摘要+向量] + D3[UPDATE session 摘要+向量] + end + + subgraph 数据库 + E[sessions 表] + F[chats 表] + end + + subgraph 向量服务 + G[硅基流动 API] + end + + subgraph 外部记忆 + H[picoclaw MEMORY.md] + end + + A --> B + B0 --> B + H --> B0 + B --> C + C --> D + D --> F + D --> E + F --> G + E --> G + + A1 -->|普通对话| B1 + A2 -->|检测关键词| B2 + A3 -->|强制触发| B3 +``` + +--- + +## 二、对话流程(默认模式) + +```mermaid +sequenceDiagram + participant U as 用户 + participant M as main.go + participant CP as GetContextPrompt + participant PicoMem as picoclaw MEMORY.md + participant AI as ProcessDirect + participant SC as SaveChat + participant DB as libSQL + participant VS as 向量服务 + + U->>M: 用户输入 + M->>CP: GetContextPrompt(userInput) + CP->>PicoMem: 读取长期记忆 + PicoMem-->>CP: 长期记忆内容 + CP->>DB: 获取 currentSession + DB->>CP: session.Summary + + Note over CP: 合并:长期记忆 + 会话摘要 + + CP->>M: 返回上下文 + M->>AI: ProcessDirect(context + input) + AI->>U: 返回 AI 回复 + + U->>SC: SaveChat(userInput, aiReply) + SC->>DB: INSERT chat + SC->>DB: UPDATE session + + Note over SC: 更新摘要和向量 + + SC->>VS: 异步生成向量 + VS-->>SC: embedding + + SC->>DB: 保存到 chats.summary_embedding + SC->>DB: 保存到 sessions.summary_embedding +``` + +--- + +## 三、四种查询场景 + +```mermaid +flowchart LR + subgraph 查询场景 + Q1[场景1: 历史摘要] + Q2[场景2: 话题检索] + Q3[场景3: 会话详情] + Q4[场景4: 会话内检索] + end + + subgraph 触发条件 + T1["之前聊过什么?"] + T2["谈论过 xxx?"] + T3["那次还说过什么?"] + T4["xxx 呢?"] + end + + subgraph 查询逻辑 + L1[查 sessions 表] + L2[chats 向量检索
Group By session_id] + L3[查 sessions 表
WHERE id = ?] + L4[chats 向量检索
WHERE session_id = ?] + end + + subgraph 返回 + R1[所有会话摘要] + R2[按 session 分组
top5 摘要拼接] + R3[指定 session 摘要] + R4[同 session 内相关摘要] + end + + T1 --> Q1 + T2 --> Q2 + T3 --> Q3 + T4 --> Q4 + + Q1 --> L1 + Q2 --> L2 + Q3 --> L3 + Q4 --> L4 + + L1 --> R1 + L2 --> R2 + L3 --> R3 + L4 --> R4 +``` + +--- + +## 四、三重检测机制 + +```mermaid +flowchart TB + I[用户输入] + + subgraph 检测层 + D1[/recall 命令] + D2[关键词匹配] + D3[向量相似度] + end + + subgraph 配置 + C1[keywords] + C2[auto_recall] + C3[similarity_threshold] + end + + I --> D1 + I --> D2 + I --> D3 + + D2 --> C1 + D3 --> C3 + + C1 -->|匹配成功| R[强制 Recall] + D3 --> C2 + + C2 -->|开启| C3 + C3 -->|阈值判断| R + + D1 -->|触发| R +``` + +--- + +## 五、数据库表结构 + +```mermaid +erDiagram + sessions { + int id PK + string uuid + text summary + blob summary_embedding + string chat_ids + int created_at + int updated_at + } + + chats { + int id PK + int session_id FK + text user_input + text ai_replies + text summary + blob summary_embedding + int created_at + int updated_at + } + + sessions ||--o{ chats : "has many" +``` + +--- + +## 六、上下文演变 + +```mermaid +flowchart LR + subgraph 时间线 + T1[开始] + T2[第1次对话] + T3[第2次对话] + T4[第N次对话] + T5[第1000次对话] + end + + subgraph 上下文状态 + S1[空] + S2[摘要1] + S3[摘要1+摘要2] + SN[摘要N] + S1000[摘要1000] + end + + subgraph 实际状态 + A1["context = 空 + 长期记忆"] + A2["context = 摘要1 + 长期记忆"] + A3["context = 摘要2 + 长期记忆"] + AN["context = 摘要N + 长期记忆"] + A1000["context = 摘要1000 + 长期记忆"] + end + + T1 --> S1 --> A1 + T2 --> S2 --> A2 + T3 --> S3 -->|覆盖| A3 + T4 --> SN -->|覆盖| AN + T5 --> S1000 -->|覆盖| A1000 + + Note over A1000: 始终只有1条会话摘要 + 长期记忆 +``` + +**注意**:长期记忆来自 `~/.picoclaw/workspace/memory/MEMORY.md`,不受会话影响,会持续保留。 + +--- + +## 七、完整流程图 + +```mermaid +flowchart TB + subgraph 输入层 + INPUT[用户输入] + CMD[/recall] + KW[关键词] + end + + subgraph 检测层 + CHECK{检测查询意图} + RECALL{Recall 触发?} + end + + subgraph 上下文构建 + CONTEXT[上下文] + SESSION_SUM[当前 Session 摘要] + RECALL_RES[Recall 结果] + end + + subgraph AI 层 + AI[ProcessDirect] + RESP[AI 回复] + end + + subgraph 保存层 + SAVE[SaveChat] + INSERT_CHAT[INSERT chat] + UPDATE_CHAT[UPDATE chat] + UPDATE_SESSION[UPDATE session] + end + + subgraph 数据库 + DB[(libSQL)] + SESSIONS[sessions 表] + CHATS[chats 表] + end + + subgraph 向量服务 + VS[向量服务] + EMB[Generate Embedding] + end + + INPUT --> CHECK + CMD --> CHECK + KW --> CHECK + + CHECK -->|普通对话| RECALL + CHECK -->|是查询| RECALL_RES + + RECALL -->|否| SESSION_SUM + RECALL_RES --> CONTEXT + + SESSION_SUM --> CONTEXT + CONTEXT --> AI + INPUT --> AI + + AI --> RESP + RESP --> SAVE + + SAVE --> INSERT_CHAT + INSERT_CHAT --> DB + DB --> CHATS + + SAVE --> UPDATE_CHAT + UPDATE_CHAT --> DB + + SAVE --> UPDATE_SESSION + UPDATE_SESSION --> DB + + CHATS --> EMB + SESSIONS --> EMB + + EMB --> VS + VS --> CHATS + VS --> SESSIONS +``` + +--- + +## 八、关键文件对应关系 + +| 模块 | 文件 | 职责 | +|------|------|------| +| 主流程 | `main.go` | 调用 GetContextPrompt、SaveChat | +| 配置 | `internal/config.go` | RecallConfig、VectorConfig | +| 数据库 | `internal/memory/db.go` | CRUD 操作 | +| 模型 | `internal/memory/model.go` | Session、Chat 结构体 | +| 向量 | `internal/memory/vector.go` | 硅基流动 API 调用 | +| 保存 | `internal/memory/save.go` | SaveChat、三重检测、长期记忆读取 | +| 查询 | `internal/memory/skill.go` | 4 个 Recall 函数 | +| 导出 | `internal/memory/export.go` | JSON 导出 | + +--- + +## 九、双记忆系统合并 + +### 记忆来源 + +| 类型 | 来源 | 持久性 | +|------|------|--------| +| **长期记忆** | `~/.picoclaw/workspace/memory/MEMORY.md` | 跨会话,AI 自动更新 | +| **会话摘要** | `~/.config/hxclaw/hxclaw.db` sessions 表 | 当前会话,动态更新 | +| **聊天详情** | `~/.config/hxclaw/hxclaw.db` chats 表 | 所有历史,支持向量检索 | + +### 上下文注入格式 + +```markdown +=== 长期记忆 === +(picoclaw MEMORY.md 内容) + +=== 当前会话摘要 === +(hxclaw sessions 表中的摘要) + +用户新问题: xxx +``` + +### 实现原理 + +`GetContextPrompt()` 函数在构建上下文时: +1. 读取 picoclaw 的 `MEMORY.md` 文件 +2. 解析并提取有效内容(跳过 Markdown 标题) +3. 合并到上下文顶部 +4. 追加会话摘要 +5. 如有 recall 结果,继续追加 + +### 数据流向 + +``` +picoclaw MEMORY.md ──┐ + ├──→ GetContextPrompt ──→ AI 上下文 +hxclaw sessions ────┘ +``` + +--- + +## 十、配置项说明 + +```yaml +memory: + recall: + keywords: ["之前", "聊过", "记得", "找找", "曾经", "谈论过", "提过"] + auto_recall: true # 自动相似度检测 + similarity_threshold: 0.7 # 相似度阈值 + max_results: 5 # 最大检索结果 + + vector: + max_search_results: 10 # 向量检索最大结果 +``` + +--- + +## 十一、MongoDB 风格导出 + +### 导出文件 + +- 路径:`~/.config/hxclaw/export-data.json` +- 格式:每次退出时自动增量导出 + +### JSON 结构 + +```json +{ + "version": 1, + "exported_at": "2026-04-27T06:12:22+08:00", + "sessions": [ + { + "id": 1, + "uuid": "session-uuid", + "summary": "会话摘要...", + "chat_ids": [1, 2, 3], + "created_at": 1740000000, + "updated_at": 1740000000, + "chats": [ + { + "id": 1, + "session_id": 1, + "user_input": "用户输入", + "ai_replies": ["AI回复1", "AI回复2"], + "summary": "摘要", + "created_at": 1740000000, + "updated_at": 1740000000 + } + ] + } + ] +} +``` + +### 设计特点 + +| 特性 | 说明 | +|------|------| +| 嵌套结构 | chats 嵌套在 sessions 下,类似 MongoDB 文档 | +| 增量导出 | 同 session 的 chats 累加,不重复创建 | +| UUID 匹配 | 按 UUID 判断是新建还是更新 | +| 版本控制 | version 字段支持格式演进 | +``` \ No newline at end of file diff --git a/docs/memory-plan.md b/docs/memory-plan.md new file mode 100644 index 0000000..fd19c90 --- /dev/null +++ b/docs/memory-plan.md @@ -0,0 +1,260 @@ +# hxclaw 聊天记忆体实现计划 + +## 概述 + +hxclaw 聊天记忆体是一个基于 libSQL (TursoDB) 的对话上下文管理系统,用于替代 picoclaw 原有的 memory.md 文件,实现更灵活、可控的会话历史管理。 + +**注意**:本计划文档已实现完成,详细功能请参考 `docs/discussion.md`。 + +## 技术选型 + +| 组件 | 选型 | 说明 | +|------|------|------| +| 数据库 | libSQL (嵌入式) | 本地文件数据库 `~/.config/hxclaw/hxclaw.db` | +| 向量模型 | BAAI/bge-m3 | 1024 维,硅基流动 API | +| UUID | github.com/google/uuid | 生成唯一会话 ID | +| 第三方库 | go_modules/ | 本地化方案解决网络问题 | + +## 数据库设计 + +### sessions 表 +```sql +CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT UNIQUE NOT NULL, + summary TEXT, + summary_embedding BLOB, + chat_ids TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); +``` + +### chats 表 +```sql +CREATE TABLE IF NOT EXISTS chats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL, + user_input TEXT NOT NULL, + ai_replies TEXT, + summary TEXT, + summary_embedding BLOB, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY(session_id) REFERENCES sessions(id) +); +``` + +## 核心流程 + +1. **Session 管理**:自动创建(首次输入)+ 手动创建(`/new`) +2. **消息保存**:在 AI 返回后调用 `SaveChat()` 保存到数据库 +3. **查询命令**:`/memory`, `/sessions`, `/new` + +## 配置 + +- **启用**:默认启用 +- **数据库路径**:`~/.config/hxclaw/hxclaw.db` +- **用户配置**:`~/.config/hxclaw/config.yml` + +## 实现状态 + +| 功能 | 状态 | +|------|------| +| 数据库 CRUD | ✅ 完成 | +| 聊天记录保存 | ✅ 完成 | +| 命令支持 | ✅ 完成 | +| JSON 导出 | ✅ 完成 | +| 向量服务 | ⚠️ 框架完成,待完善 | +| 向量查询 | ⏳ 待实现 | + +hxclaw 聊天记忆体是一个基于 TursoDB (libSQL) 的对话上下文管理系统,用于替代 picoclaw 原有的 memory.md 文件,实现更灵活、可控的会话历史管理。 + +核心目标: +1. 保留 picoclaw 的 memory.md 作为 AI 长期记忆 +2. 新增对话记忆体作为会话维度的短期记忆 +3. 支持向量检索和多场景查询 + +## 技术选型 + +| 组件 | 选型 | 说明 | +|------|------|------| +| 数据库 | libSQL (嵌入式) | 本地文件数据库,支持 JSON 导出 | +| 向量模型 | BAAI/bge-m3 | 1024 维,硅基流动 API | +| UUID | github.com/google/uuid | 生成唯一会话 ID | +| libSQL 驱动 | github.com/tursodatabase/go-libsql | CGO 需启用 | + +## 数据库设计 + +### 表结构 + +```sql +-- sessions 表(会话维度) +CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT UNIQUE NOT NULL, -- 外键引用用 UUID + summary TEXT, -- 会话压缩后的摘要 + summary_embedding BLOB, -- 1024维向量(二进制) + chat_ids TEXT, -- JSON数组 ["id1","id2",...] + created_at INTEGER NOT NULL, -- Unix时间戳 + updated_at INTEGER NOT NULL +); + +-- chats 表(消息维度) +CREATE TABLE IF NOT EXISTS chats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL, -- 外键到 sessions.id + user_input TEXT NOT NULL, -- 用户输入 + ai_replies TEXT, -- JSON数组 [{"role":"assistant","content":"..."},...] + summary TEXT, -- 单条消息摘要 + summary_embedding BLOB, -- 1024维向量 + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY(session_id) REFERENCES sessions(id) +); + +-- 索引 +CREATE INDEX IF NOT EXISTS idx_chats_session_id ON chats(session_id); +CREATE INDEX IF NOT EXISTS idx_sessions_uuid ON sessions(uuid); +``` + +### 数据模型 + +```go +// Session 会话记录 +type Session struct { + ID int64 `json:"id"` + UUID string `json:"uuid"` + Summary string `json:"summary"` + SummaryEmbedding []byte `json:"-"` + ChatIDs []int64 `json:"chat_ids"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// Chat 聊天记录 +type Chat struct { + ID int64 `json:"id"` + SessionID int64 `json:"session_id"` + UserInput string `json:"user_input"` + AIReplies []string `json:"ai_replies"` + Summary string `json:"summary"` + SummaryEmbedding []byte `json:"-"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} +``` + +## 核心流程 + +### 1. Session 管理 + +#### 自动开始(模式 A) +- 用户首次输入内容时,检查是否存在活跃 Session +- 若无,则自动创建新 Session + +#### 手动开始(模式 B) +- 用户输入 `/new` 命令,强制创建新 Session + +### 2. 消息处理流程 + +``` +用户输入 + ↓ +[可选] 创建新 Session(首次输入时) + ↓ +chats 表插入记录 (session_id, user_input, created_at) + ↓ +AI 处理 + ↓ +AI 返回后: + 1. 生成摘要 + 向量 + 2. chats 表更新 (ai_replies, summary, summary_embedding) + 3. sessions 表更新 (chat_ids 追加, summary 压缩, summary_embedding 更新) + ↓ +将 Session 摘要作为上下文注入 AI +``` + +### 3. 上下文注入 + +``` +=== 当前会话摘要 === +[最新摘要内容] +=== +``` + +### 4. 查询场景(4种) + +| 场景 | 用户意图 | 查询方式 | +|------|----------|----------| +| 1 | "之前聊过什么" | 查询最新 Session 的 summary | +| 2 | "某某某相关内容" | 向量搜索 top 5 | +| 3 | "某次谈论时的详情" | 定位 Session ID,返回 summary | +| 4 | 同 Session 内其他相关内容 | session_id 过滤 + 向量搜索 | + +## 模块设计 + +### 目录结构 + +``` +cmd/hxclaw/internal/memory/ +├── db.go # 数据库初始化和 CRUD 操作 +├── model.go # 数据模型定义 +├── vector.go # 向量服务(硅基流动 API) +├── skill.go # 内置 Skill(查询逻辑) +└── export.go # 导出功能 +``` + +## 配置项 + +在 `project.config.yml` 中新增: + +```yaml +# 聊天记忆体配置 +memory: + enabled: true # 启用开关 + db_path: "./hxclaw.db" # 数据库路径 + auto_session: true # 自动创建 Session + export_on_exit: true # 退出时导出 + +# 向量服务配置 +vector: + api_key: "sk-xxx" # 硅基流动 API Key + base_url: "https://api.siliconflow.cn/v1" + model: "BAAI/bge-m3" # 向量模型 + dimensions: 1024 # 向量维度 +``` + +## 实现步骤 + +### 阶段一:基础设施 +1. 新增依赖并测试连接(go-libsql, google/uuid) +2. 创建 `memory/model.go` - 数据模型 +3. 创建 `memory/db.go` - CRUD 操作 + +### 阶段二:向量服务 +4. 创建 `memory/vector.go` - 硅基流动 Embedding API + +### 阶段三:核心逻辑 +5. 修改 `main.go` - 集成 Session +6. 修改消息处理流程 - 存储和摘要 +7. 创建 `memory/skill.go` - 查询逻辑 + +### 阶段四:UI 和导出 +8. 添加命令支持(`/memory`, `/sessions`) +9. 创建 `memory/export.go` - JSON 导出 + +## 依赖 + +```bash +go get github.com/tursodatabase/go-libsql +go get github.com/google/uuid +go get github.com/sjzsdu/langchaingo-cn/llms/siliconflow +``` + +## 注意事项 + +1. **CGO 要求**:`go-libsql` 需要 CGO 启用 +2. **向量维度**:BGE-M3 默认 1024 维 +3. **摘要生成**:使用 AI 返回内容的前 N 字符作为摘要 +4. **错误处理**:向量服务失败时降级为纯文本搜索 \ No newline at end of file diff --git a/docs/picoclaw-research.md b/docs/picoclaw-research.md new file mode 100644 index 0000000..8ee6480 --- /dev/null +++ b/docs/picoclaw-research.md @@ -0,0 +1,238 @@ +# picoclaw 技术研究报告 + +## 概述 + +本报告基于对 picoclaw v0.2.6 源代码的研究,详细分析其核心机制。 + +--- + +## 1. 系统架构 + +### 1.1 核心模块 + +``` +picoclaw/ +├── pkg/ +│ ├── agent/ # AI 代理核心逻辑 +│ ├── providers # LLM 提供者 +│ ├── config/ # 配置管理 +│ ├── tools/ # 工具注册与执行 +│ ├── skills/ # Skill 加载器 +│ ├── bus/ # 消息总线 +│ └── channels/ # 消息通道 +└── web/ # Web 界面 +``` + +### 1.2 依赖关系 + +- **agent**:核心模块,依赖 providers、tools、skills、bus +- **providers**:LLM API 调用(OpenAI、Anthropic 等) +- **tools**:工具执行(exec、read_file、web_fetch 等) +- **skills**:通过 SKILL.md 加载技能定义 + +--- + +## 2. Skill 机制 + +### 2.1 加载机制 + +**位置**:`pkg/skills/loader.go` + +Skill 存储在三个位置,优先级: +1. `workspace/skills/` - 项目级 +2. `~/.picoclaw/skills/` - 全局 +3. 内置 skills 目录 + +每个 skill 必须是 `SKILL.md` 文件,包含: +- Frontmatter (YAML/JSON) 定义 `name` 和 `description` +- Markdown 主体内容(操作指导) + +### 2.2 执行机制 + +**关键发现**:Skill 本质是 Markdown 文档,不是可执行代码 + +AI 阅读 SKILL.md 后,调用实际工具执行任务: +``` +加载 SKILL.md → AI 阅读 → 调用工具 → 执行结果 +``` + +--- + +## 3. 工具执行机制 + +### 3.1 执行流程 + +**位置**:`pkg/agent/loop.go:runTurn` (约 2332-2899 行) + +```go +// 串行执行每个工具调用 +for i, tc := range normalizedToolCalls { + toolName := tc.Name + toolArgs := cloneStringAnyMap(tc.Arguments) + + // 执行工具 + toolResult := ts.agent.Tools.ExecuteWithContext( + execCtx, toolName, toolArgs, ts.channel, ts.chatID, asyncCallback, + ) + + // 结果添加到消息历史 + toolResultMsg := providers.Message{ + Role: "tool", + Content: contentForLLM, + ToolCallID: toolCallID, + } + messages = append(messages, toolResultMsg) +} +``` + +### 3.2 关键特性 + +| 特性 | 说明 | +|------|------| +| 执行方式 | **串行执行**,非并行 | +| 结果收集 | 通过 `providers.Message` 添加到上下文 | +| 用户显示 | `ForUser` 字段非空时发送给用户 | +| 静默结果 | `SilentResult()` 不显示给用户,但发给 LLM | + +### 3.3 工具结果标志 + +```go +// Silent: 不发送消息给用户,但发送给 LLM +Silent bool `json:"silent"` + +// ResponseHandled: 工具已处理响应,不生成独立消息 +ResponseHandled bool `json:"response_handled,omitempty"` + +// IsError: 标记为错误结果 +IsError bool `json:"is_error,omitempty"` +``` + +--- + +## 4. 消息处理 + +### 4.1 消息总线 + +**位置**:`pkg/bus/bus.go` + +``` +用户输入 → AgentLoop.ProcessDirect() → AI 处理 → 工具执行 ← + ↓ + bus.PublishOutbound() → 输出 +``` + +### 4.2 消息类型 + +| 类型 | 说明 | +|------|------| +| InboundMessage | 用户输入 | +| OutboundMessage | AI 输出 | +| ToolResult | 工具结果 | + +--- + +## 5. 异步执行 + +### 5.1 AsyncExecutor 接口 + +**位置**:`pkg/tools/base.go:107` + +```go +type AsyncExecutor interface { + Tool + ExecuteAsync(ctx context.Context, args map[string]any, cb AsyncCallback) *ToolResult +} +``` + +### 5.2 使用方式 + +```go +// 执行时传入回调函数 +toolResult := ts.agent.Tools.ExecuteWithContext( + execCtx, toolName, toolArgs, ts.channel, ts.chatID, + asyncCallback, // 异步回调 +) +``` + +--- + +## 6. TTS 工具分析 + +### 6.1 文件位置 + +- 主文件:`pkg/tools/tts_send.go` +- TTS 提供者:`pkg/audio/tts/tts.go` + +### 6.2 实现方式 + +1. 通过 `SendTTSTool` 执行 TTS 合成 +2. 调用 `tts.SynthesizeAndStore()` 生成音频 +3. 返回文件路径或直接播放 + +### 6.3 已知问题 + +| 问题 | 原因 | +|------|------| +| 播放内容不一致 | 异步回调竞争 | +| 结果不显示 | 使用了 `SilentResult()` | +| 多条记录只显示一条 | 串行执行中的状态竞争 | + +--- + +## 7. 配置系统 + +### 7.1 配置文件 + +- 项目级:`config.yaml` +- 用户级:`~/.picoclaw/config.json` +- 环境变量:`PICOCLAW_*` 前缀 + +### 7.2 配置结构 + +```go +type Config struct { + Agents AgentsConfig `json:"agents"` + Tools ToolsConfig `json:"tools"` + Channels ChannelsConfig `json:"channels"` + Voice VoiceConfig `json:"voice"` +} +``` + +--- + +## 8. hxclaw 集成方式 + +### 8.1 复用策略 + +hxclaw 通过 Go replace 机制复用 picoclaw: + +```go +// go.mod +replace github.com/sipeed/picoclaw => ./path/to/picoclaw +``` + +### 8.2 核心依赖 + +- `pkg/agent` - AI 代理核心 +- `pkg/providers` - LLM 提供者 +- `pkg/config` - 配置加载 +- `pkg/bus` - 消息总线 + +--- + +## 9. 总结 + +| 模块 | 机制 | 阻塞 | +|------|------|------| +| Skill | Markdown 文档 | 否 | +| 工具 | 串行执行 | 是 | +| 异步工具 | 回调机制 | 可选 | +| 消息总线 | 非阻塞 | 否 | + +--- + +## 10. 建议 + +1. **避免阻塞**:长时间任务使用 AsyncExecutor +2. **结果显示**:检查 Silent/ResponseHandled 标志 +3. **并发控制**:使用 SubTurn 的 concurrencySem \ No newline at end of file diff --git a/go.mod b/go.mod index 2425f6a..7bd40f8 100644 --- a/go.mod +++ b/go.mod @@ -6,17 +6,24 @@ require ( charm.land/bubbles/v2 v2.1.0 charm.land/glamour/v2 v2.0.0 charm.land/lipgloss/v2 v2.0.2 + github.com/charmbracelet/x/term v0.2.2 github.com/ergochat/readline v0.1.3 + github.com/google/uuid v1.6.0 github.com/muesli/termenv v0.16.0 github.com/sipeed/picoclaw v0.2.6 + github.com/tursodatabase/libsql-client-go v0.0.0-00010101000000-000000000000 gopkg.in/yaml.v3 v3.0.1 ) +// 使用本地第三方库 +replace github.com/tursodatabase/libsql-client-go => ./go_modules/libsql-client-go + require ( charm.land/bubbletea/v2 v2.0.2 // indirect github.com/adhocore/gronx v1.19.6 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect @@ -40,11 +47,11 @@ require ( github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/coder/websocket v1.8.14 // indirect github.com/creack/pty v1.1.24 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -53,7 +60,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect github.com/google/jsonschema-go v0.4.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/h2non/filetype v1.1.3 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect @@ -86,6 +92,7 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect @@ -96,5 +103,5 @@ require ( modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.48.2 // indirect + modernc.org/sqlite v1.48.0 // indirect ) diff --git a/go.sum b/go.sum index e01f695..aea7792 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= @@ -76,6 +78,8 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -195,8 +199,8 @@ golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -212,8 +216,8 @@ golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -243,8 +247,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= -modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4= +modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/go_modules/libsql-client-go b/go_modules/libsql-client-go new file mode 160000 index 0000000..236aa1f --- /dev/null +++ b/go_modules/libsql-client-go @@ -0,0 +1 @@ +Subproject commit 236aa1ff8accc5180a51c25b437c91630ac05221 diff --git a/project.config.yml b/project.config.yml index 163d9f9..cce7cc5 100644 --- a/project.config.yml +++ b/project.config.yml @@ -2,13 +2,13 @@ # 模拟流式输出配置 streaming: - line_delay_ms: 1000 # 每行输出后的延迟(毫秒) - last_line_delay_ms: 600 # 最后一行延迟(毫秒) + line_delay_ms: 1000 + last_line_delay_ms: 600 # Markdown 渲染配置 markdown: - theme: dark # 渲染主题:dark, light, dracula, tokyo-night 等 - line_width: -1 # 自动换行宽度(-1=禁用,0=自动,>0=固定宽度) + theme: dark + line_width: -1 # UI 配置 ui: @@ -17,6 +17,17 @@ ui: # TTS 语音配置 tts: - enabled: false # 全局开关(默认关闭) - port: 9876 # mimo-tts daemon 端口 - auto: true # AI 回复后自动朗读 \ No newline at end of file + enabled: false + port: 9876 + auto: true + +# 聊天记忆体配置 +memory: + enabled: false + auto_session: true + auto_export: true + vector: + base_url: "https://api.siliconflow.cn/v1" + model: "BAAI/bge-m3" + dimension: 1024 + max_search_results: 10 \ No newline at end of file diff --git a/taolun.md b/taolun.md index 325241d..b49c1dc 100644 --- a/taolun.md +++ b/taolun.md @@ -455,4 +455,105 @@ func (r *Readline) SetPrompt(prompt string) error { func (r *Readline) SetPrompt(prompt string) { r.rl.SetPrompt(prompt) // void 类型 } -``` \ No newline at end of file +``` + +--- + +### 20. 禁用 picoclaw session 历史,实现独立上下文系统 + +#### 问题背景 + +- picoclaw 的 session 历史会被自动清空 +- 不利于 hxclaw 的会话连续性 +- 需要实现自我控制的上下文系统 + +#### 解决方案 + +- 禁用 picoclaw 的 session 历史读取 +- 使用 hxclaw 自己的 libSQL 数据库存储会话摘要 +- 在 ProcessDirect() 调用前注入上下文摘要到用户输入 + +#### 实现步骤 + +1. **创建 GetContextPrompt()** - `cmd/hxclaw/internal/memory/save.go` + ```go + func GetContextPrompt() string { + // 从 hxclaw 自己的数据库获取会话摘要 + return "=== 当前会话摘要 === +" + session.Summary + " +============ +" + } + ``` + +2. **注入上下文** - `cmd/hxclaw/main.go` + ```go + if memoryCfg.Enabled { + contextPrompt := memory.GetContextPrompt() + if contextPrompt != "" { + input = contextPrompt + " +用户新问题: " + input + } + } + resp, err := agentLoop.ProcessDirect(context.Background(), input, sessionKey) + ``` + +#### 效果 + +- hxclaw 完全独立于 picoclaw session 管理 +- 会话摘要通过数据库持久化 +- 上下文通过摘要注入传递 + +--- + +### 21. UI 合并显示与颜色设计 + +#### 需求 + +将原来分两行显示的信息合并为一行: +- 之前:`[memory] 已保存,当前会话 8 条消息` + `▣ 耗时: 20.3s` +- 之后:`▣ 耗时: 20.3s · 会话已保存 · 当前会话 8 条消息` + +#### 颜色设计 + +| 文字 | 颜色 | 十六进制 | +|------|------|---------| +| ▣ (图标) | 金色 | #f0c75e | +| 灰色文字 | 灰色 | #2b2e32 | +| 会话已保存 | 暗绿色 | #4a9e6b | +| 会话保存异常 | 暗红色 | #c75050 | + +#### 代码实现 + +```go +var ( + iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e")) + textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32")) + memoryOkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#4a9e6b")) + memoryErrStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#c75050")) +) + +func printElapsed(elapsed time.Duration, chatCount int, saveErr error) { + icon := iconStyle.Render("▣ ") + timeText := textStyle.Render(fmt.Sprintf("耗时: %s", elapsedStr)) + + var statusText string + if saveErr != nil { + statusText = memoryErrStyle.Render("会话保存异常") + } else if chatCount > 0 { + statusText = memoryOkStyle.Render("会话已保存") + } + + memCountText := textStyle.Render(fmt.Sprintf("当前会话 %d 条消息", chatCount)) + fmt.Printf(" %s%s · %s · %s + +", icon, timeText, statusText, memCountText) +} +``` + +#### 关键点 + +- `SaveChat()` 改为返回 `(chatCount int, err error)`,便于错误处理 +- 状态文字单独使用颜色样式 +- 失败时显示"会话保存异常"(暗红色) +