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 7f94926970
5 changed files with 75 additions and 5 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.DS_Store .DS_Store
*.ini *.ini
hxclaw
hxclaw.exe hxclaw.exe
.idea/ .idea/

View File

@@ -106,6 +106,16 @@
--- ---
## 构建命令
```bash
go build -o hxclaw ./cmd/hxclaw
```
生成的可执行文件位于项目根目录。
---
## 注意事项 ## 注意事项
- 不要修改 picoclaw 源码 - 不要修改 picoclaw 源码
- 保持代码独立,便于后续版本同步 - 保持代码独立,便于后续版本同步

View File

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

6
go.mod
View File

@@ -4,14 +4,15 @@ go 1.25.9
require ( require (
charm.land/bubbles/v2 v2.1.0 charm.land/bubbles/v2 v2.1.0
charm.land/bubbletea/v2 v2.0.2 charm.land/glamour/v2 v2.0.0
charm.land/lipgloss/v2 v2.0.2 charm.land/lipgloss/v2 v2.0.2
github.com/ergochat/readline v0.1.3 github.com/ergochat/readline v0.1.3
github.com/muesli/termenv v0.16.0
github.com/sipeed/picoclaw v0.0.0 github.com/sipeed/picoclaw v0.0.0
) )
require ( require (
charm.land/glamour/v2 v2.0.0 // indirect charm.land/bubbletea/v2 v2.0.2 // indirect
github.com/adhocore/gronx v1.19.6 // indirect github.com/adhocore/gronx v1.19.6 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect
@@ -61,7 +62,6 @@ require (
github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/modelcontextprotocol/go-sdk v1.5.0 // indirect github.com/modelcontextprotocol/go-sdk v1.5.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/openai/openai-go/v3 v3.22.0 // indirect github.com/openai/openai-go/v3 v3.22.0 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect

6
go.sum
View File

@@ -8,8 +8,12 @@ charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc= github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc=
github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
@@ -109,6 +113,8 @@ github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=