feat: 配置系统重构,添加用户配置和中文注释

This commit is contained in:
2026-04-23 20:11:22 +08:00
parent e070461fe4
commit dd3c8a03e1
10 changed files with 336 additions and 318 deletions

View File

@@ -1,3 +1,5 @@
// Package internal 包含 hxclaw 的内部工具模块
// 提供配置管理、Markdown 渲染、输入读取等功能
package internal
import (
@@ -8,105 +10,235 @@ import (
"gopkg.in/yaml.v3"
)
type ProjectConfig struct {
// Config 是项目配置结构体包含流式输出、Markdown 渲染和 UI 相关配置
type Config struct {
// Streaming 流式输出配置
Streaming StreamingConfig `yaml:"streaming"`
Markdown MarkdownConfig `yaml:"markdown"`
UI UIConfig `yaml:"ui"`
// Markdown Markdown 渲染配置
Markdown MarkdownConfig `yaml:"markdown"`
// UI UI 显示配置
UI UIConfig `yaml:"ui"`
}
// StreamingConfig 流式输出配置,控制模拟打字效果的延迟时间
type StreamingConfig struct {
LineDelayMs int `yaml:"line_delay_ms"`
// LineDelayMs 每行输出延迟(毫秒)
LineDelayMs int `yaml:"line_delay_ms"`
// LastLineDelayMs 最后一行输出延迟(毫秒)
LastLineDelayMs int `yaml:"last_line_delay_ms"`
}
// MarkdownConfig Markdown 渲染配置,控制渲染主题和换行行为
type MarkdownConfig struct {
GlamourStyle string `yaml:"glamour_style"`
WrapWidth int `yaml:"wrap_width"`
// 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 string `yaml:"logo"`
UserPrefix string `yaml:"user_prefix"`
// Logo 应用 Logo 字符
Logo string `yaml:"logo"`
// UserIcon 用户输入提示符
UserIcon string `yaml:"user_icon"`
}
var (
defaultCfg = ProjectConfig{
// defaultCfg 默认配置值,当配置文件不存在或字段为空时使用
defaultCfg = Config{
Streaming: StreamingConfig{
LineDelayMs: 1000,
LastLineDelayMs: 600,
},
Markdown: MarkdownConfig{
GlamourStyle: "dark",
WrapWidth: 0,
Theme: "dark",
LineWidth: 0,
},
UI: UIConfig{
Logo: "🦐",
UserPrefix: "👀 ",
Logo: "🦐",
UserIcon: "👀 ",
},
}
projCfg *ProjectConfig
projCfgLock sync.RWMutex
cfg *Config // 已合并的配置(用户配置 + 项目配置)
cfgLock sync.RWMutex // 配置读写锁
userCfg *Config // 用户配置文件解析结果
userLock sync.RWMutex // 用户配置读写锁
)
func LoadProjectConfig() error {
projCfgLock.Lock()
defer projCfgLock.Unlock()
// 用户配置文件路径常量
const (
userConfigDir = ".config/hxclaw" // 用户配置目录(相对于用户家目录)
userConfigFile = "config.yml" // 用户配置文件名
)
cfgPath := getConfigPath()
if cfgPath == "" {
projCfg = &defaultCfg
// 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) {
projCfg = &defaultCfg
return nil
return &defaultCfg
}
return err
}
var cfg ProjectConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return err
}
if cfg.Streaming.LineDelayMs <= 0 {
cfg.Streaming.LineDelayMs = defaultCfg.Streaming.LineDelayMs
}
if cfg.Streaming.LastLineDelayMs <= 0 {
cfg.Streaming.LastLineDelayMs = defaultCfg.Streaming.LastLineDelayMs
}
if cfg.Markdown.GlamourStyle == "" {
cfg.Markdown.GlamourStyle = defaultCfg.Markdown.GlamourStyle
}
if cfg.Markdown.WrapWidth < 0 {
cfg.Markdown.WrapWidth = 0
}
if cfg.UI.Logo == "" {
cfg.UI.Logo = defaultCfg.UI.Logo
}
if cfg.UI.UserPrefix == "" {
cfg.UI.UserPrefix = defaultCfg.UI.UserPrefix
}
projCfg = &cfg
return nil
}
func GetProjectConfig() *ProjectConfig {
projCfgLock.RLock()
defer projCfgLock.RUnlock()
if projCfg == nil {
return &defaultCfg
}
return projCfg
var projCfg Config
if err := yaml.Unmarshal(data, &projCfg); err != nil {
return &defaultCfg
}
return &projCfg
}
func getConfigPath() string {
// 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")
}
}

View File

@@ -1,3 +1,5 @@
// Package internal 包含 hxclaw 的内部工具模块
// 提供配置管理、Markdown 渲染、输入读取等功能
package internal
import (
@@ -9,6 +11,8 @@ import (
"github.com/charmbracelet/x/term"
)
// RenderMarkdown 将 Markdown 文本渲染为终端友好的格式
// 支持通过配置或环境变量指定渲染主题和换行宽度
func RenderMarkdown(md string) string {
if md == "" {
return ""
@@ -67,8 +71,8 @@ func RenderParagraph(text string) string {
func getStyle() string {
if cfg := GetProjectConfig(); cfg != nil {
if cfg.Markdown.GlamourStyle != "" {
return cfg.Markdown.GlamourStyle
if cfg.Markdown.Theme != "" {
return cfg.Markdown.Theme
}
}
if s := os.Getenv("GLAMOUR_STYLE"); s != "" {
@@ -79,10 +83,10 @@ func getStyle() string {
func getWrapWidth() int {
if cfg := GetProjectConfig(); cfg != nil {
if cfg.Markdown.WrapWidth > 0 {
return cfg.Markdown.WrapWidth
if cfg.Markdown.LineWidth > 0 {
return cfg.Markdown.LineWidth
}
if cfg.Markdown.WrapWidth < 0 {
if cfg.Markdown.LineWidth < 0 {
return 0
}
}

View File

@@ -1,3 +1,5 @@
// Package internal 包含 hxclaw 的内部工具模块
// 提供配置管理、Markdown 渲染、输入读取等功能
package internal
import (
@@ -9,22 +11,26 @@ import (
"charm.land/lipgloss/v2"
)
// SpinnerState 表示加载动画的状态
type SpinnerState int
const (
StateThinking SpinnerState = iota
StateAnswering
StateDone
StateThinking SpinnerState = iota // 思考中状态
StateAnswering // 回答中状态
StateDone // 完成状态
)
// Spinner 加载动画组件,用于显示思考状态
type Spinner struct {
text string
state SpinnerState
spinner spinner.Model
stopCh chan struct{}
doneCh chan struct{}
text string // 显示的文本内容
state SpinnerState // 当前状态
spinner spinner.Model // 底层动画模型
stopCh chan struct{} // 停止信号通道
doneCh chan struct{} // 完成信号通道
}
// NewSpinner 创建一个新的加载动画实例
// text: 初始显示的文本内容
func NewSpinner(text string) *Spinner {
s := spinner.New(
spinner.WithSpinner(spinner.MiniDot),