package main import ( "context" "fmt" "math" "os" "strings" "time" "charm.land/lipgloss/v2" "github.com/hxclaw/hxclaw/cmd/hxclaw/internal" "github.com/muesli/termenv" "github.com/sipeed/picoclaw/pkg/agent" "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/providers" ) const Logo = "🦐" func main() { 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 { fmt.Fprintf(os.Stderr, "错误:加载配置失败: %v\n", err) os.Exit(1) } logger.ConfigureFromEnv() provider, modelID, err := providers.CreateProvider(cfg) if err != nil { fmt.Fprintf(os.Stderr, "错误:创建 Provider 失败: %v\n", err) os.Exit(1) } if modelID != "" { cfg.Agents.Defaults.ModelName = modelID } msgBus := bus.NewMessageBus() defer msgBus.Close() agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) defer agentLoop.Close() startupInfo := agentLoop.GetStartupInfo() logger.InfoCF("hxclaw", "HxClaw 已初始化", map[string]any{ "tools_count": startupInfo["tools"].(map[string]any)["count"], "skills_total": startupInfo["skills"].(map[string]any)["total"], "skills_available": startupInfo["skills"].(map[string]any)["available"], }) fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", Logo) interactiveMode(agentLoop, "cli:default") } func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) { prompt := internal.GetProjectConfig().UI.UserPrefix rl, err := internal.NewReadline(prompt) if err != nil { fmt.Printf("初始化 readline 失败: %v\n", err) fmt.Println("回退到简单输入模式...") simpleInteractiveMode(agentLoop, sessionKey) return } defer rl.Close() for { line, err := rl.Readline() if err != nil { if err == internal.ErrInterrupt || err == internal.ErrEOF { fmt.Println("\n再见!") return } fmt.Printf("读取输入错误: %v\n", err) continue } input := line if input == "" { continue } if input == "exit" || input == "quit" { fmt.Println("再见!") return } runWithStreaming(agentLoop, input, sessionKey) } } func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { reader := internal.NewSimpleReader() for { fmt.Print(internal.GetProjectConfig().UI.UserPrefix) line, err := reader.ReadString() if err != nil { if err == internal.ErrEOF { fmt.Println("\n再见!") return } fmt.Printf("读取输入错误: %v\n", err) continue } input := line if input == "" { continue } if input == "exit" || input == "quit" { fmt.Println("再见!") return } runWithStreaming(agentLoop, input, sessionKey) } } // runWithStreaming 使用 ProcessDirect 处理请求,支持工具调用和结果显示 func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) { startTime := time.Now() spinner := internal.NewSpinner("思考中...") spinner.Start() resp, err := agentLoop.ProcessDirect(context.Background(), input, sessionKey) spinner.Stop() if err != nil { fmt.Printf("错误: %v\n", err) return } rendered := internal.RenderMarkdown(resp) clearSpinnerLine() outputLineByLine(rendered) elapsed := time.Since(startTime) printElapsed(elapsed) } func clearSpinnerLine() { output := termenv.DefaultOutput() output.ClearLine() fmt.Print("\r") os.Stdout.Sync() } func outputLineByLine(text string) { if text == "" { return } lines := strings.Split(text, "\n") totalLines := len(lines) cfg := internal.GetProjectConfig() lineDelay := time.Duration(cfg.Streaming.LineDelayMs) * time.Millisecond lastLineDelay := time.Duration(cfg.Streaming.LastLineDelayMs) * time.Millisecond for i, line := range lines { if line == "" { fmt.Println() continue } fmt.Println(line) if i < totalLines-1 { time.Sleep(lineDelay) } else { time.Sleep(lastLineDelay) } } fmt.Println() } var ( iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e")) textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32")) ) func printElapsed(elapsed time.Duration) { elapsedSec := math.Round(elapsed.Seconds()*10) / 10 elapsedStr := formatDuration(elapsedSec) icon := iconStyle.Render("▣ ") text := textStyle.Render(fmt.Sprintf("耗时: %s", elapsedStr)) 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) }