244 lines
6.3 KiB
Go
244 lines
6.3 KiB
Go
// 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"`
|
||
}
|
||
|
||
// 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"`
|
||
}
|
||
|
||
var (
|
||
// defaultCfg 默认配置值,当配置文件不存在或字段为空时使用
|
||
defaultCfg = Config{
|
||
Streaming: StreamingConfig{
|
||
LineDelayMs: 1000,
|
||
LastLineDelayMs: 600,
|
||
},
|
||
Markdown: MarkdownConfig{
|
||
Theme: "dark",
|
||
LineWidth: 0,
|
||
},
|
||
UI: UIConfig{
|
||
Logo: "🦐",
|
||
UserIcon: "👀 ",
|
||
},
|
||
}
|
||
cfg *Config // 已合并的配置(用户配置 + 项目配置)
|
||
cfgLock sync.RWMutex // 配置读写锁
|
||
userCfg *Config // 用户配置文件解析结果
|
||
userLock sync.RWMutex // 用户配置读写锁
|
||
)
|
||
|
||
// 用户配置文件路径常量
|
||
const (
|
||
userConfigDir = ".config/hxclaw" // 用户配置目录(相对于用户家目录)
|
||
userConfigFile = "config.yml" // 用户配置文件名
|
||
)
|
||
|
||
// LoadProjectConfig 加载并合并项目配置
|
||
// 加载顺序:用户配置(~/.config/hxclaw/config.yml) > 项目配置(project.config.yml)
|
||
// 最终配置由两者合并得出,用户配置优先于项目配置
|
||
func LoadProjectConfig() error {
|
||
cfgLock.Lock()
|
||
defer cfgLock.Unlock()
|
||
userLock.Lock()
|
||
defer userLock.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 &defaultCfg
|
||
}
|
||
return nil
|
||
}
|
||
|
||
var userCfg Config
|
||
if err := yaml.Unmarshal(data, &userCfg); err != nil {
|
||
return nil
|
||
}
|
||
|
||
return &userCfg
|
||
}
|
||
|
||
// createUserConfig 创建默认的用户配置文件
|
||
// userPath: 配置文件完整路径
|
||
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 # 渲染主题:dark, light, dracula, tokyo-night 等
|
||
line_width: 0 # 自动换行宽度(0=自动,>0=固定宽度,-1=禁用)
|
||
|
||
# UI 配置
|
||
ui:
|
||
logo: "🦐"
|
||
user_icon: "👀 "
|
||
`
|
||
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 userCfg != nil {
|
||
if userCfg.Markdown.Theme != "" {
|
||
result.Markdown.Theme = userCfg.Markdown.Theme
|
||
}
|
||
if userCfg.Markdown.LineWidth != 0 {
|
||
result.Markdown.LineWidth = userCfg.Markdown.LineWidth
|
||
}
|
||
if userCfg.UI.Logo != "" {
|
||
result.UI.Logo = userCfg.UI.Logo
|
||
}
|
||
if userCfg.UI.UserIcon != "" {
|
||
result.UI.UserIcon = userCfg.UI.UserIcon
|
||
}
|
||
}
|
||
|
||
return &result
|
||
}
|
||
|
||
// GetProjectConfig 获取当前生效的配置(线程安全)
|
||
// 如果配置未加载,返回默认配置
|
||
func GetProjectConfig() *Config {
|
||
cfgLock.RLock()
|
||
defer cfgLock.RUnlock()
|
||
if cfg == nil {
|
||
return &defaultCfg
|
||
}
|
||
return cfg
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// getProjectConfigPath 获取项目配置文件路径
|
||
// 优先使用环境变量 HXCLAW_CONFIG 指定的路径,否则使用当前目录的 project.config.yml
|
||
func getProjectConfigPath() string {
|
||
if path := os.Getenv("HXCLAW_CONFIG"); path != "" {
|
||
return path
|
||
}
|
||
return filepath.Join(".", "project.config.yml")
|
||
} |