diff --git a/.gitignore b/.gitignore index 8084426..28a7c0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store *.ini +hxclaw hxclaw.exe .idea/ diff --git a/agents.md b/agents.md index ea606b7..b78a792 100644 --- a/agents.md +++ b/agents.md @@ -106,6 +106,16 @@ --- +## 构建命令 + +```bash +go build -o hxclaw ./cmd/hxclaw +``` + +生成的可执行文件位于项目根目录。 + +--- + ## 注意事项 - 不要修改 picoclaw 源码 - 保持代码独立,便于后续版本同步 diff --git a/cmd/hxclaw/main.go b/cmd/hxclaw/main.go index 81419de..c3e0ab6 100644 --- a/cmd/hxclaw/main.go +++ b/cmd/hxclaw/main.go @@ -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) +} diff --git a/go.mod b/go.mod index 5d489fd..bb55b93 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,15 @@ go 1.25.9 require ( 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 github.com/ergochat/readline v0.1.3 + github.com/muesli/termenv v0.16.0 github.com/sipeed/picoclaw v0.0.0 ) 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/alecthomas/chroma/v2 v2.14.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/modelcontextprotocol/go-sdk v1.5.0 // 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/openai/openai-go/v3 v3.22.0 // indirect github.com/pion/randutil v0.1.0 // indirect diff --git a/go.sum b/go.sum index b247361..7ec035c 100644 --- a/go.sum +++ b/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= github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc= 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/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/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= 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/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/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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=