- 使用 ProcessDirect 替代 ChatStream,支持工具调用结果显示 - 新增 project.config.yml 统一配置(Logo、用户前缀、流式延迟、Markdown等) - Markdown 渲染支持自动终端宽度换行 - 按行输出文本,每行延迟可配置 - 简化状态栏,只显示耗时(图标颜色 #f0c75e,文字颜色 #2b2e32) - spinner 动画右移两个字符 - 用户输入前缀可配置化
This commit is contained in:
@@ -18,12 +18,16 @@ import (
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
)
|
||||
|
||||
var totalCompletionTokens int
|
||||
|
||||
const Logo = "🦐"
|
||||
|
||||
func main() {
|
||||
fmt.Printf("%s HxClaw - PicoClaw 增强版 CLI\n\n", Logo)
|
||||
if err := internal.LoadProjectConfig(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "错误:加载项目配置失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logo := internal.GetProjectConfig().UI.Logo
|
||||
fmt.Printf("%s HxClaw - PicoClaw 增强版 CLI\n\n", logo)
|
||||
|
||||
cfg, err := internal.LoadConfig()
|
||||
if err != nil {
|
||||
@@ -62,7 +66,7 @@ func main() {
|
||||
}
|
||||
|
||||
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
prompt := fmt.Sprintf("%s You: ", Logo)
|
||||
prompt := internal.GetProjectConfig().UI.UserPrefix
|
||||
|
||||
rl, err := internal.NewReadline(prompt)
|
||||
if err != nil {
|
||||
@@ -101,7 +105,7 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
reader := internal.NewSimpleReader()
|
||||
for {
|
||||
fmt.Print(fmt.Sprintf("%s You: ", Logo))
|
||||
fmt.Print(internal.GetProjectConfig().UI.UserPrefix)
|
||||
line, err := reader.ReadString()
|
||||
if err != nil {
|
||||
if err == internal.ErrEOF {
|
||||
@@ -126,131 +130,78 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
}
|
||||
}
|
||||
|
||||
// runWithStreaming 尝试使用流式输出,如果 Provider 不支持则回退到普通模式
|
||||
// runWithStreaming 使用 ProcessDirect 处理请求,支持工具调用和结果显示
|
||||
func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
|
||||
startTime := time.Now()
|
||||
|
||||
agentInstance := agentLoop.GetRegistry().GetDefaultAgent()
|
||||
if agentInstance == nil {
|
||||
fmt.Println("错误:无法获取 Agent 实例")
|
||||
spinner := internal.NewSpinner("思考中...")
|
||||
spinner.Start()
|
||||
|
||||
resp, err := agentLoop.ProcessDirect(context.Background(), input, sessionKey)
|
||||
|
||||
spinner.Stop()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("错误: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
provider := agentInstance.Provider
|
||||
ctx := context.Background()
|
||||
rendered := internal.RenderMarkdown(resp)
|
||||
clearSpinnerLine()
|
||||
outputLineByLine(rendered)
|
||||
|
||||
// 判断是否支持流式
|
||||
if sp, ok := provider.(providers.StreamingProvider); ok {
|
||||
// 从 session 中获取历史消息
|
||||
history := agentInstance.Sessions.GetHistory(sessionKey)
|
||||
summary := agentInstance.Sessions.GetSummary(sessionKey)
|
||||
elapsed := time.Since(startTime)
|
||||
printElapsed(elapsed)
|
||||
}
|
||||
|
||||
// 使用 ContextBuilder 构建消息,包含历史
|
||||
messages := agentInstance.ContextBuilder.BuildMessages(
|
||||
history,
|
||||
summary,
|
||||
input,
|
||||
nil, // media
|
||||
"cli", // channel
|
||||
sessionKey,
|
||||
"", // senderID
|
||||
"", // senderDisplayName
|
||||
)
|
||||
func clearSpinnerLine() {
|
||||
output := termenv.DefaultOutput()
|
||||
output.ClearLine()
|
||||
fmt.Print("\r")
|
||||
os.Stdout.Sync()
|
||||
}
|
||||
|
||||
// 获取工具定义
|
||||
toolDefs := agentInstance.Tools.ToProviderDefs()
|
||||
func outputLineByLine(text string) {
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 启动 spinner,显示 "思考中..."
|
||||
spinner := internal.NewSpinner("思考中...")
|
||||
spinner.Start()
|
||||
lines := strings.Split(text, "\n")
|
||||
totalLines := len(lines)
|
||||
|
||||
fmt.Print("\n")
|
||||
var result strings.Builder
|
||||
var printedLen int
|
||||
firstToken := true
|
||||
resp, err := sp.ChatStream(ctx, messages, toolDefs, agentInstance.Model, nil, func(accumulated string) {
|
||||
if firstToken && len(accumulated) > 0 {
|
||||
spinner.Stop()
|
||||
firstToken = false
|
||||
}
|
||||
if len(accumulated) > printedLen {
|
||||
newText := accumulated[printedLen:]
|
||||
fmt.Print(newText)
|
||||
os.Stdout.Sync()
|
||||
result.WriteString(newText)
|
||||
printedLen = len(accumulated)
|
||||
}
|
||||
})
|
||||
cfg := internal.GetProjectConfig()
|
||||
lineDelay := time.Duration(cfg.Streaming.LineDelayMs) * time.Millisecond
|
||||
lastLineDelay := time.Duration(cfg.Streaming.LastLineDelayMs) * time.Millisecond
|
||||
|
||||
if err != nil {
|
||||
spinner.Stop()
|
||||
fmt.Printf("流式调用错误: %v\n", err)
|
||||
return
|
||||
for i, line := range lines {
|
||||
if line == "" {
|
||||
fmt.Println()
|
||||
continue
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
fmt.Println(line)
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
printStats(resp, elapsed)
|
||||
|
||||
agentInstance.Sessions.AddMessage(sessionKey, "user", input)
|
||||
agentInstance.Sessions.AddMessage(sessionKey, "assistant", allOutput)
|
||||
}
|
||||
} else {
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||
if err != nil {
|
||||
fmt.Printf("错误: %v\n", err)
|
||||
return
|
||||
}
|
||||
rendered := internal.RenderMarkdown(response)
|
||||
if rendered != "" && rendered != response {
|
||||
fmt.Printf("\n%s\n\n", rendered)
|
||||
if i < totalLines-1 {
|
||||
time.Sleep(lineDelay)
|
||||
} else {
|
||||
fmt.Printf("\n%s %s\n\n", Logo, response)
|
||||
time.Sleep(lastLineDelay)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
var (
|
||||
iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffcc80"))
|
||||
textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#5c7a9a"))
|
||||
iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e"))
|
||||
textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32"))
|
||||
)
|
||||
|
||||
func printStats(resp *providers.LLMResponse, elapsed time.Duration) {
|
||||
if resp == nil || resp.Usage == nil {
|
||||
return
|
||||
}
|
||||
|
||||
completionTokens := resp.Usage.CompletionTokens
|
||||
if completionTokens <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
totalCompletionTokens += completionTokens
|
||||
|
||||
func printElapsed(elapsed time.Duration) {
|
||||
elapsedSec := math.Round(elapsed.Seconds()*10) / 10
|
||||
|
||||
thisTokens := formatTokens(completionTokens)
|
||||
totalTokens := formatTokens(totalCompletionTokens)
|
||||
elapsedStr := formatDuration(elapsedSec)
|
||||
|
||||
icon := iconStyle.Render("▣ ")
|
||||
text := textStyle.Render(fmt.Sprintf("Tokens: %s · 耗时: %s · 总Tokens: %s", thisTokens, elapsedStr, totalTokens))
|
||||
text := textStyle.Render(fmt.Sprintf("耗时: %s", elapsedStr))
|
||||
fmt.Printf(" %s%s\n\n", icon, text)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user