feat: 优化流式输出,添加按行延迟和配置化
Some checks failed
Release / build (push) Failing after 22s

- 使用 ProcessDirect 替代 ChatStream,支持工具调用结果显示
- 新增 project.config.yml 统一配置(Logo、用户前缀、流式延迟、Markdown等)
- Markdown 渲染支持自动终端宽度换行
- 按行输出文本,每行延迟可配置
- 简化状态栏,只显示耗时(图标颜色 #f0c75e,文字颜色 #2b2e32)
- spinner 动画右移两个字符
- 用户输入前缀可配置化
This commit is contained in:
2026-04-15 06:13:52 +08:00
parent 432a8db938
commit bdad44de7d
7 changed files with 260 additions and 123 deletions

View File

@@ -0,0 +1,112 @@
package internal
import (
"os"
"path/filepath"
"sync"
"gopkg.in/yaml.v3"
)
type ProjectConfig struct {
Streaming StreamingConfig `yaml:"streaming"`
Markdown MarkdownConfig `yaml:"markdown"`
UI UIConfig `yaml:"ui"`
}
type StreamingConfig struct {
LineDelayMs int `yaml:"line_delay_ms"`
LastLineDelayMs int `yaml:"last_line_delay_ms"`
}
type MarkdownConfig struct {
GlamourStyle string `yaml:"glamour_style"`
WrapWidth int `yaml:"wrap_width"`
}
type UIConfig struct {
Logo string `yaml:"logo"`
UserPrefix string `yaml:"user_prefix"`
}
var (
defaultCfg = ProjectConfig{
Streaming: StreamingConfig{
LineDelayMs: 1000,
LastLineDelayMs: 600,
},
Markdown: MarkdownConfig{
GlamourStyle: "dark",
WrapWidth: 0,
},
UI: UIConfig{
Logo: "🦐",
UserPrefix: "👀 ",
},
}
projCfg *ProjectConfig
projCfgLock sync.RWMutex
)
func LoadProjectConfig() error {
projCfgLock.Lock()
defer projCfgLock.Unlock()
cfgPath := getConfigPath()
if cfgPath == "" {
projCfg = &defaultCfg
return nil
}
data, err := os.ReadFile(cfgPath)
if err != nil {
if os.IsNotExist(err) {
projCfg = &defaultCfg
return nil
}
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
}
func getConfigPath() string {
if path := os.Getenv("HXCLAW_CONFIG"); path != "" {
return path
}
return filepath.Join(".", "project.config.yml")
}

View File

@@ -2,9 +2,11 @@ package internal
import (
"os"
"strconv"
"strings"
"charm.land/glamour/v2"
"github.com/charmbracelet/x/term"
)
func RenderMarkdown(md string) string {
@@ -13,10 +15,11 @@ func RenderMarkdown(md string) string {
}
style := getStyle()
wrapWidth := getWrapWidth()
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle(style),
glamour.WithWordWrap(80),
glamour.WithWordWrap(wrapWidth),
)
if err != nil {
return md
@@ -43,10 +46,11 @@ func RenderParagraph(text string) string {
}
style := getStyle()
wrapWidth := getWrapWidth()
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle(style),
glamour.WithWordWrap(80),
glamour.WithWordWrap(wrapWidth),
)
if err != nil {
return text
@@ -62,9 +66,33 @@ func RenderParagraph(text string) string {
}
func getStyle() string {
style := "dark"
if s := os.Getenv("GLAMOUR_STYLE"); s != "" {
style = s
if cfg := GetProjectConfig(); cfg != nil {
if cfg.Markdown.GlamourStyle != "" {
return cfg.Markdown.GlamourStyle
}
}
return style
if s := os.Getenv("GLAMOUR_STYLE"); s != "" {
return s
}
return "dark"
}
func getWrapWidth() int {
if cfg := GetProjectConfig(); cfg != nil {
if cfg.Markdown.WrapWidth > 0 {
return cfg.Markdown.WrapWidth
}
}
if cols := os.Getenv("COLUMNS"); 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 width
}

View File

@@ -80,11 +80,11 @@ func (s *Spinner) run() {
}
func (s *Spinner) render() {
fmt.Printf("\r%s %s", s.spinner.View(), s.text)
fmt.Printf("\r %s %s", s.spinner.View(), s.text)
os.Stdout.Sync()
}
func (s *Spinner) clear() {
fmt.Printf("\r%s 思考完成.\n", s.spinner.View())
fmt.Printf("\r %s 思考完成.\n", s.spinner.View())
os.Stdout.Sync()
}