feat: 添加 tokens 统计和耗时显示功能

- 添加进程级别累计 CompletionTokens 统计
- 显示此次消耗 tokens 和耗时
- 显示累计 tokens(hxclaw 进程级别)
- 使用 lipgloss 样式(icon #ffcc80, text #5c7a9a)
- 更新 AGENTS.md 构建说明
This commit is contained in:
2026-04-12 03:04:54 +08:00
parent 1568c63462
commit f85092175e
5 changed files with 75 additions and 5 deletions

View File

@@ -3,8 +3,12 @@ package main
import (
"context"
"fmt"
"math"
"os"
"strings"
"time"
"charm.land/lipgloss/v2"
"github.com/hxclaw/hxclaw/cmd/hxclaw/internal"
"github.com/muesli/termenv"
@@ -14,6 +18,8 @@ import (
"github.com/sipeed/picoclaw/pkg/providers"
)
var totalCompletionTokens int
const Logo = "🦐"
func main() {
@@ -122,6 +128,8 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
// runWithStreaming 尝试使用流式输出,如果 Provider 不支持则回退到普通模式
func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
startTime := time.Now()
agentInstance := agentLoop.GetRegistry().GetDefaultAgent()
if agentInstance == nil {
fmt.Println("错误:无法获取 Agent 实例")
@@ -160,7 +168,7 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
var result strings.Builder
var printedLen int
firstToken := true
_, err := sp.ChatStream(ctx, messages, toolDefs, agentInstance.Model, nil, func(accumulated string) {
resp, err := sp.ChatStream(ctx, messages, toolDefs, agentInstance.Model, nil, func(accumulated string) {
if firstToken && len(accumulated) > 0 {
spinner.Stop()
firstToken = false
@@ -175,6 +183,7 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
})
if err != nil {
spinner.Stop()
fmt.Printf("流式调用错误: %v\n", err)
return
}
@@ -183,7 +192,6 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
allOutput := result.String()
rendered := internal.RenderMarkdown(allOutput)
if rendered != allOutput && rendered != "" {
// 计算流式输出的行数,清除
lines := strings.Count(allOutput, "\n") + 1
output := termenv.DefaultOutput()
output.CursorUp(1)
@@ -197,6 +205,9 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
fmt.Println()
}
elapsed := time.Since(startTime)
printStats(resp, elapsed)
agentInstance.Sessions.AddMessage(sessionKey, "user", input)
agentInstance.Sessions.AddMessage(sessionKey, "assistant", allOutput)
}
@@ -214,3 +225,45 @@ func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
}
}
}
var (
iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffcc80"))
textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#5c7a9a"))
)
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
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))
fmt.Printf(" %s%s\n\n", icon, text)
}
func formatTokens(n int) string {
if n >= 1000 {
return fmt.Sprintf("%.1fk", float64(n)/1000)
}
return fmt.Sprintf("%d", n)
}
func formatDuration(s float64) string {
if s >= 60 {
return fmt.Sprintf("%.1fm", s/60)
}
return fmt.Sprintf("%.1fs", s)
}