Files
HxClaw/cmd/hxclaw/internal/config.go
titor 5d9498f687
Some checks failed
Release / build (push) Failing after 4m28s
feat: 记忆体系统 v0.3.0 完成
## 核心功能
- 双记忆系统合并: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
2026-04-27 06:16:19 +08:00

435 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package internal 包含 hxclaw 的内部工具模块
// 提供配置管理、Markdown 渲染、输入读取等功能
package internal
import (
"os"
"path/filepath"
"sync"
"gopkg.in/yaml.v3"
)
// Config 是项目配置结构体包含流式输出、Markdown 渲染和 UI 相关配置
type Config struct {
// Streaming 流式输出配置
Streaming StreamingConfig `yaml:"streaming"`
// Markdown Markdown 渲染配置
Markdown MarkdownConfig `yaml:"markdown"`
// UI UI 显示配置
UI UIConfig `yaml:"ui"`
// TTS TTS 语音配置
TTS TTSConfig `yaml:"tts"`
// Memory 聊天记忆体配置
Memory MemoryConfig `yaml:"memory"`
}
// StreamingConfig 流式输出配置,控制模拟打字效果的延迟时间
type StreamingConfig struct {
// LineDelayMs 每行输出延迟(毫秒)
LineDelayMs int `yaml:"line_delay_ms"`
// LastLineDelayMs 最后一行输出延迟(毫秒)
LastLineDelayMs int `yaml:"last_line_delay_ms"`
}
// MarkdownConfig Markdown 渲染配置,控制渲染主题和换行行为
type MarkdownConfig struct {
// Theme 渲染主题,支持 dark、light、dracula、tokyo-night 等
Theme string `yaml:"theme"`
// LineWidth 自动换行宽度0=自动,>0=固定宽度,-1=禁用)
LineWidth int `yaml:"line_width"`
}
// UIConfig UI 显示配置,控制 Logo 和用户提示符
type UIConfig struct {
// Logo 应用 Logo 字符
Logo string `yaml:"logo"`
// UserIcon 用户输入提示符
UserIcon string `yaml:"user_icon"`
}
// TTSConfig TTS 语音配置
type TTSConfig struct {
// Enabled 全局开关
Enabled bool `yaml:"enabled"`
// Port 端口
Port int `yaml:"port"`
// Auto AI 回复后自动朗读
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 <20><>量维度
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{
Streaming: StreamingConfig{
LineDelayMs: 1000,
LastLineDelayMs: 600,
},
Markdown: MarkdownConfig{
Theme: "dark",
LineWidth: -1,
},
UI: UIConfig{
Logo: "🦐",
UserIcon: "👀 ",
},
TTS: TTSConfig{
Enabled: false,
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 // 配置读写锁
)
// 用户配置文件路径常量
const (
userConfigFile = "config.yml" // 用户配置文件名
)
// LoadProjectConfig 加载并合并项目配置
// 加载顺序:用户配置(~/.config/hxclaw/config.yml > 项目配置project.config.yml
// 最终配置由两者合并得出,用户配置优先于项目配置
func LoadProjectConfig() error {
cfgLock.Lock()
defer cfgLock.Unlock()
userCfg := loadUserConfig()
projCfg := loadProjectConfig()
merged := mergeConfig(userCfg, projCfg)
cfg = merged
return nil
}
// loadUserConfig 加载用户配置文件
// 路径:~/.config/hxclaw/config.yml
// 如果文件不存在,则自动创建默认配置文件
func loadUserConfig() *Config {
userPath := getUserConfigPath()
if userPath == "" {
return nil
}
data, err := os.ReadFile(userPath)
if err != nil {
if os.IsNotExist(err) {
createUserConfig(userPath)
return nil
}
return nil
}
var userCfg Config
if err := yaml.Unmarshal(data, &userCfg); err != nil {
return nil
}
return &userCfg
}
// createUserConfig 创建默认的用户配置文件
func createUserConfig(userPath string) error {
dir := filepath.Dir(userPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
defaultContent := `# hxclaw 用户配置文件
# 此文件位于 ~/.config/hxclaw/config.yml
# 用户配置优先于项目配置
# Markdown 渲染配置
markdown:
theme: dark
line_width: 0
# UI 配置
ui:
logo: "🦐"
user_icon: "👀 "
# TTS 语音配置
tts:
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)
}
// loadProjectConfig 加载项目级配置文件
// 路径优先级:环境变量 HXCLAW_CONFIG 指定路径 > ./project.config.yml
func loadProjectConfig() *Config {
cfgPath := GetProjectConfigPath()
if cfgPath == "" {
return &defaultCfg
}
data, err := os.ReadFile(cfgPath)
if err != nil {
if os.IsNotExist(err) {
return &defaultCfg
}
return &defaultCfg
}
var projCfg Config
if err := yaml.Unmarshal(data, &projCfg); err != nil {
return &defaultCfg
}
return &projCfg
}
// mergeConfig 合并用户配置和项目配置
// 合并规则:用户配置优先于项目配置,项目配置优先于默认配置
func mergeConfig(userCfg, projCfg *Config) *Config {
result := defaultCfg
// 先应用项目配置
if projCfg != nil {
if projCfg.Streaming.LineDelayMs > 0 {
result.Streaming.LineDelayMs = projCfg.Streaming.LineDelayMs
}
if projCfg.Streaming.LastLineDelayMs > 0 {
result.Streaming.LastLineDelayMs = projCfg.Streaming.LastLineDelayMs
}
if projCfg.Markdown.Theme != "" {
result.Markdown.Theme = projCfg.Markdown.Theme
}
if projCfg.Markdown.LineWidth != 0 {
result.Markdown.LineWidth = projCfg.Markdown.LineWidth
}
if projCfg.UI.Logo != "" {
result.UI.Logo = projCfg.UI.Logo
}
if projCfg.UI.UserIcon != "" {
result.UI.UserIcon = projCfg.UI.UserIcon
}
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
}
}
// 再应用用户配置(覆盖项目配置)
if userCfg != nil {
// Markdown
if userCfg.Markdown.Theme != "" {
result.Markdown.Theme = userCfg.Markdown.Theme
}
if userCfg.Markdown.LineWidth != 0 {
result.Markdown.LineWidth = userCfg.Markdown.LineWidth
}
// UI
if userCfg.UI.Logo != "" {
result.UI.Logo = userCfg.UI.Logo
}
if userCfg.UI.UserIcon != "" {
result.UI.UserIcon = userCfg.UI.UserIcon
}
// TTS用户配置可以覆盖 enabled 和 auto
if userCfg.TTS.Enabled {
result.TTS.Enabled = userCfg.TTS.Enabled
}
if userCfg.TTS.Port > 0 {
result.TTS.Port = userCfg.TTS.Port
}
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
}
// GetProjectConfig 获取当前生效的配置(线程安全)
// 如果配置未加载,返回默认配置
func GetProjectConfig() *Config {
cfgLock.RLock()
defer cfgLock.RUnlock()
if cfg == nil {
return &defaultCfg
}
return cfg
}
// getUserConfigPath 获取用户配置文件路径
// 路径格式:~/.config/hxclaw/config.yml
func getUserConfigPath() string {
return GetConfigFile()
}
// GetUserConfigDir 获取用户配置目录
func GetUserConfigDir() string {
return GetConfigDir()
}
func GetProjectConfigPath() string {
if path := os.Getenv("HXCLAW_CONFIG"); path != "" {
return path
}
return filepath.Join(".", "project.config.yml")
}