feat: 添加 tokens 统计和耗时显示功能
- 添加进程级别累计 CompletionTokens 统计 - 显示此次消耗 tokens 和耗时 - 显示累计 tokens(hxclaw 进程级别) - 使用 lipgloss 样式(icon #ffcc80, text #5c7a9a) - 更新 AGENTS.md 构建说明
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.ini
|
*.ini
|
||||||
|
|
||||||
|
hxclaw
|
||||||
hxclaw.exe
|
hxclaw.exe
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
10
agents.md
10
agents.md
@@ -106,6 +106,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 构建命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o hxclaw ./cmd/hxclaw
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的可执行文件位于项目根目录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
- 不要修改 picoclaw 源码
|
- 不要修改 picoclaw 源码
|
||||||
- 保持代码独立,便于后续版本同步
|
- 保持代码独立,便于后续版本同步
|
||||||
|
|||||||
@@ -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
6
go.mod
@@ -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
6
go.sum
@@ -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=
|
||||||
|
|||||||
Reference in New Issue
Block a user