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)`,便于错误处理
+- 状态文字单独使用颜色样式
+- 失败时显示"会话保存异常"(暗红色)
+