From 88a110e87e3aa5c8bcf5d739e03b7ed4c04f00d8 Mon Sep 17 00:00:00 2001 From: "Z.To" Date: Sun, 26 Apr 2026 06:44:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=20markdown=20?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E4=BF=AE=E5=A4=8D=EF=BC=8C=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=B3=BB=E7=BB=9F=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复 e070461 修复:wrap_width=-1 禁用换行,使用 lipgloss.Print - 应用 dd3c8a0 配置重构:字段名变更(theme/line_width/user_icon) - 添加用户配置文件支持(~/.config/hxclaw/config.yml) - 添加 TTS 配置到用户配置(enabled, auto) --- agents.md | 38 ++++- cmd/hxclaw/internal/config.go | 285 ++++++++++++++++++++++++-------- cmd/hxclaw/internal/markdown.go | 19 ++- cmd/hxclaw/main.go | 10 +- project.config.yml | 16 +- 5 files changed, 278 insertions(+), 90 deletions(-) diff --git a/agents.md b/agents.md index 3956be6..73dca7d 100644 --- a/agents.md +++ b/agents.md @@ -97,18 +97,19 @@ streaming: # Markdown 渲染配置 markdown: - glamour_style: dark # 渲染主题:dark, light, dracula, tokyo-night 等 + theme: dark # 渲染主题:dark, light, dracula, tokyo-night 等 + line_width: -1 # 自动换行宽度(-1=禁用,0=自动,>0=固定宽度) # UI 配置 ui: logo: "🦐" - user_prefix: "👀 " + user_icon: "👀 " # TTS 语音配置 tts: - enabled: false # 全局开关(默认关闭) - port: 9876 # mimo-tts daemon 端口 - auto: true # AI 回复后自动朗读 + enabled: false # 全局开关(默认关闭) + port: 9876 # mimo-tts daemon 端口 + auto: true # AI 回复后自动朗读 ``` 配置加载优先级: @@ -117,6 +118,33 @@ tts: --- +### 用户配置 + +用户配置文件位于 `~/.config/hxclaw/config.yml`,启动时自动创建。 + +```yaml +# hxclaw 用户配置文件 +# 此文件位于 ~/.config/hxclaw/config.yml +# 用户配置优先于项目配置 + +# Markdown 渲染配置 +markdown: + theme: dark # 渲染主题 + line_width: 0 # 换行宽度 + +# UI 配置 +ui: + logo: "🦐" + user_icon: "👀 " + +# TTS 语音配置 +tts: + enabled: false # 全局开关 + auto: true # AI 回复后自动朗读 +``` + +--- + ## TTS 使用指南 ### 命令 diff --git a/cmd/hxclaw/internal/config.go b/cmd/hxclaw/internal/config.go index 68f28c3..c7a8b32 100644 --- a/cmd/hxclaw/internal/config.go +++ b/cmd/hxclaw/internal/config.go @@ -1,3 +1,5 @@ +// Package internal 包含 hxclaw 的内部工具模块 +// 提供配置管理、Markdown 渲染、输入读取等功能 package internal import ( @@ -8,47 +10,66 @@ 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"` - TTS TTSConfig `yaml:"tts"` + // Markdown Markdown 渲染配置 + Markdown MarkdownConfig `yaml:"markdown"` + // UI UI 显示配置 + UI UIConfig `yaml:"ui"` + // TTS TTS 语音配置 + TTS TTSConfig `yaml:"tts"` } +// 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"` } +// TTSConfig TTS 语音配置 type TTSConfig struct { + // Enabled 全局开关 Enabled bool `yaml:"enabled"` - Port int `yaml:"port"` - Auto bool `yaml:"auto"` + // Port 端口 + Port int `yaml:"port"` + // Auto AI 回复后自动朗读 + Auto bool `yaml:"auto"` } var ( - defaultCfg = ProjectConfig{ + // defaultCfg 默认配置值,当配置文件不存在或字段为空时使用 + defaultCfg = Config{ Streaming: StreamingConfig{ LineDelayMs: 1000, LastLineDelayMs: 600, }, Markdown: MarkdownConfig{ - GlamourStyle: "dark", - WrapWidth: 0, + Theme: "dark", + LineWidth: -1, }, UI: UIConfig{ - Logo: "🦐", - UserPrefix: "👀 ", + Logo: "🦐", + UserIcon: "👀 ", }, TTS: TTSConfig{ Enabled: false, @@ -56,72 +77,202 @@ var ( Auto: true, }, } - projCfg *ProjectConfig - projCfgLock sync.RWMutex + cfg *Config // 已合并的配置(用户配置 + 项目配置) + cfgLock 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() + + 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 # 渲染主题:dark, light, dracula, tokyo-night 等 + line_width: 0 # 自动换行宽度(-1=禁用,0=自动,>0=固定宽度) + +# UI 配置 +ui: + logo: "🦐" + user_icon: "👀 " + +# TTS 语音配置 +tts: + enabled: false # 全局开关(默认关闭) + auto: true # AI 回复后自动朗读 +` + 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 - } - if cfg.TTS.Port <= 0 { - cfg.TTS.Port = defaultCfg.TTS.Port - } - - 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 projCfg.TTS.Port > 0 { + result.TTS.Port = projCfg.TTS.Port + } + } + + // 再应用用户配置(覆盖项目配置) + 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 + } + } + + 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") -} +} \ No newline at end of file diff --git a/cmd/hxclaw/internal/markdown.go b/cmd/hxclaw/internal/markdown.go index f5fc0c3..318fe71 100644 --- a/cmd/hxclaw/internal/markdown.go +++ b/cmd/hxclaw/internal/markdown.go @@ -67,8 +67,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,8 +79,11 @@ 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.LineWidth < 0 { + return 0 } } @@ -90,9 +93,15 @@ func getWrapWidth() int { } } + if cols := os.Getenv("LINES"); cols != "" { + if w, err := strconv.Atoi(cols); err == nil && w > 0 { + return w + } + } + width, _, err := term.GetSize(0) if err != nil || width <= 0 { - return 80 + return 0 } return width } diff --git a/cmd/hxclaw/main.go b/cmd/hxclaw/main.go index c307eed..29f68fa 100644 --- a/cmd/hxclaw/main.go +++ b/cmd/hxclaw/main.go @@ -66,7 +66,7 @@ func main() { } func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) { - basePrompt := internal.GetProjectConfig().UI.UserPrefix + basePrompt := internal.GetProjectConfig().UI.UserIcon prompt := internal.GetTTSPrompt(basePrompt) rl, err := internal.NewReadline(prompt) @@ -134,7 +134,7 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { internal.SetTTSEnabled(true) } for { - fmt.Print(internal.GetTTSPrompt(internal.GetProjectConfig().UI.UserPrefix)) + fmt.Print(internal.GetTTSPrompt(internal.GetProjectConfig().UI.UserIcon)) line, err := reader.ReadString() if err != nil { if err == internal.ErrEOF { @@ -225,11 +225,11 @@ func outputLineByLine(text string) { for i, line := range lines { if line == "" { - fmt.Println() + lipgloss.Print("\n") continue } - fmt.Println(line) + lipgloss.Print(line + "\n") if i < totalLines-1 { time.Sleep(lineDelay) @@ -238,7 +238,7 @@ func outputLineByLine(text string) { } } - fmt.Println() + lipgloss.Print("\n") } var ( diff --git a/project.config.yml b/project.config.yml index f67dfbd..163d9f9 100644 --- a/project.config.yml +++ b/project.config.yml @@ -2,21 +2,21 @@ # 模拟流式输出配置 streaming: - line_delay_ms: 1000 # 每行输出后的延迟(毫秒) - last_line_delay_ms: 600 # 最后一行延迟(毫秒) + line_delay_ms: 1000 # 每行输出后的延迟(毫秒) + last_line_delay_ms: 600 # 最后一行延迟(毫秒) # Markdown 渲染配置 markdown: - glamour_style: dark # 渲染主题:dark, light, dracula, tokyo-night 等 - wrap_width: 0 # 自动换行宽度(0=自动获取终端宽度) + theme: dark # 渲染主题:dark, light, dracula, tokyo-night 等 + line_width: -1 # 自动换行宽度(-1=禁用,0=自动,>0=固定宽度) # UI 配置 ui: logo: "🦐" - user_prefix: "👀 " + user_icon: "👀 " # TTS 语音配置 tts: - enabled: false # 全局开关(默认关闭) - port: 9876 # mimo-tts daemon 端口 - auto: true # AI 回复后自动朗读 \ No newline at end of file + enabled: false # 全局开关(默认关闭) + port: 9876 # mimo-tts daemon 端口 + auto: true # AI 回复后自动朗读 \ No newline at end of file