// 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"` // SummaryTimeout 摘要生成超时时间(秒) SummaryTimeout int `yaml:"summary_timeout"` // 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{ 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, SummaryTimeout: 20, 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 summary_timeout: 30 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.SummaryTimeout > 0 { result.Memory.SummaryTimeout = projCfg.Memory.SummaryTimeout } 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") }