feat: 添加 Markdown 终端渲染支持(glamour)
This commit is contained in:
@@ -118,3 +118,38 @@ func (r *SimpleReader) ReadString() (string, error) {
|
||||
}
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func FindParagraphEnd(text string, startPos int) int {
|
||||
if startPos >= len(text) {
|
||||
return 0
|
||||
}
|
||||
|
||||
inCodeBlock := false
|
||||
inMathBlock := false
|
||||
|
||||
for i := startPos; i < len(text); i++ {
|
||||
if i+3 < len(text) && text[i:i+3] == "```" {
|
||||
if !inCodeBlock {
|
||||
inCodeBlock = true
|
||||
} else {
|
||||
inCodeBlock = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if i+2 < len(text) && (text[i:i+2] == "$$" || text[i:i+2] == "\\[") {
|
||||
inMathBlock = !inMathBlock
|
||||
continue
|
||||
}
|
||||
|
||||
if inCodeBlock || inMathBlock {
|
||||
continue
|
||||
}
|
||||
|
||||
if i+1 < len(text) && text[i] == '\n' && text[i+1] == '\n' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
70
cmd/hxclaw/internal/markdown.go
Normal file
70
cmd/hxclaw/internal/markdown.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"charm.land/glamour/v2"
|
||||
)
|
||||
|
||||
func RenderMarkdown(md string) string {
|
||||
if md == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
style := getStyle()
|
||||
|
||||
r, err := glamour.NewTermRenderer(
|
||||
glamour.WithStandardStyle(style),
|
||||
glamour.WithWordWrap(80),
|
||||
)
|
||||
if err != nil {
|
||||
return md
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
out, err := r.Render(md)
|
||||
if err != nil {
|
||||
return md
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func RenderParagraph(text string) string {
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
text = strings.TrimRight(text, "\n")
|
||||
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
style := getStyle()
|
||||
|
||||
r, err := glamour.NewTermRenderer(
|
||||
glamour.WithStandardStyle(style),
|
||||
glamour.WithWordWrap(80),
|
||||
)
|
||||
if err != nil {
|
||||
return text
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
out, err := r.Render(text)
|
||||
if err != nil {
|
||||
return text
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func getStyle() string {
|
||||
style := "dark"
|
||||
if s := os.Getenv("GLAMOUR_STYLE"); s != "" {
|
||||
style = s
|
||||
}
|
||||
return style
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/hxclaw/hxclaw/cmd/hxclaw/internal"
|
||||
"github.com/muesli/termenv"
|
||||
"github.com/sipeed/picoclaw/pkg/agent"
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
@@ -160,15 +161,15 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
|
||||
var printedLen int
|
||||
firstToken := true
|
||||
_, err := sp.ChatStream(ctx, messages, toolDefs, agentInstance.Model, nil, func(accumulated string) {
|
||||
// 检测到第一个 token 时,停止 spinner
|
||||
if firstToken && len(accumulated) > 0 {
|
||||
spinner.Stop()
|
||||
firstToken = false
|
||||
}
|
||||
if len(accumulated) > printedLen {
|
||||
fmt.Print(accumulated[printedLen:])
|
||||
newText := accumulated[printedLen:]
|
||||
fmt.Print(newText)
|
||||
os.Stdout.Sync()
|
||||
result.WriteString(accumulated[printedLen:])
|
||||
result.WriteString(newText)
|
||||
printedLen = len(accumulated)
|
||||
}
|
||||
})
|
||||
@@ -178,21 +179,38 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
|
||||
// 将用户消息和回复保存到 session
|
||||
if result.Len() > 0 {
|
||||
allOutput := result.String()
|
||||
rendered := internal.RenderMarkdown(allOutput)
|
||||
if rendered != allOutput && rendered != "" {
|
||||
// 计算流式输出的行数,清除
|
||||
lines := strings.Count(allOutput, "\n") + 1
|
||||
output := termenv.DefaultOutput()
|
||||
output.CursorUp(1)
|
||||
output.ClearLine()
|
||||
output.ClearLines(lines)
|
||||
fmt.Print(rendered)
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
} else {
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
agentInstance.Sessions.AddMessage(sessionKey, "user", input)
|
||||
agentInstance.Sessions.AddMessage(sessionKey, "assistant", result.String())
|
||||
agentInstance.Sessions.AddMessage(sessionKey, "assistant", allOutput)
|
||||
}
|
||||
} else {
|
||||
// 回退到普通模式
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||
if err != nil {
|
||||
fmt.Printf("错误: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("\n%s %s\n\n", Logo, response)
|
||||
rendered := internal.RenderMarkdown(response)
|
||||
if rendered != "" && rendered != response {
|
||||
fmt.Printf("\n%s\n\n", rendered)
|
||||
} else {
|
||||
fmt.Printf("\n%s %s\n\n", Logo, response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user