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) { basePrompt := internal.GetProjectConfig().UI.UserPrefix prompt := internal.GetTTSPrompt(basePrompt) rl, err := internal.NewReadline(prompt) if err != nil { fmt.Printf("初始化 readline 失败: %v\n", err) fmt.Println("回退到简单输入模式...") simpleInteractiveMode(agentLoop, sessionKey) return } defer rl.Close() ttsCfg := internal.GetProjectConfig().TTS if ttsCfg.Enabled { internal.SetTTSEnabled(true) } 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 } isTempTTS := false if len(input) > 0 && input[0] == 'T' && (len(input) == 1 || input[1] == ' ') { input = strings.TrimPrefix(input, "T") input = strings.TrimPrefix(input, " ") isTempTTS = true } if strings.HasPrefix(input, "/tts") { handleTTSCommand(input, rl, basePrompt) continue } if isTempTTS { enabled := internal.ToggleTTS() if enabled { rl.SetPrompt(internal.GetTTSPrompt(basePrompt)) } } runWithStreaming(agentLoop, input, sessionKey, isTempTTS) } } func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { reader := internal.NewSimpleReader() ttsCfg := internal.GetProjectConfig().TTS if ttsCfg.Enabled { internal.SetTTSEnabled(true) } for { fmt.Print(internal.GetTTSPrompt(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 } isTempTTS := false if len(input) > 0 && input[0] == 'T' && (len(input) == 1 || input[1] == ' ') { input = strings.TrimPrefix(input, "T") input = strings.TrimPrefix(input, " ") isTempTTS = true } if strings.HasPrefix(input, "/tts") { handleTTSCommandSimple(input) continue } if isTempTTS { internal.ToggleTTS() } runWithStreaming(agentLoop, input, sessionKey, isTempTTS) } } // runWithStreaming 使用 ProcessDirect 处理请求,支持工具调用和结果显示 func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string, tempTTS bool) { 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) ttsCfg := internal.GetProjectConfig().TTS if ttsCfg.Enabled || tempTTS || internal.IsTTSEnabled() { go internal.SpeakText(resp) } 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) } func handleTTSCommand(input string, rl *internal.Readline, basePrompt string) { args := strings.Fields(input) if len(args) == 1 { enabled := internal.ToggleTTS() rl.SetPrompt(internal.GetTTSPrompt(basePrompt)) status := "关闭" if enabled { status = "开启" } fmt.Printf("TTS 已%s\n", status) return } switch args[1] { case "on": internal.SetTTSEnabled(true) rl.SetPrompt(internal.GetTTSPrompt(basePrompt)) fmt.Println("TTS 已开启") case "off": internal.SetTTSEnabled(false) rl.SetPrompt(internal.GetTTSPrompt(basePrompt)) fmt.Println("TTS 已关闭") case "status": status := "关闭" if internal.IsTTSEnabled() { status = "开启" } fmt.Printf("TTS 状态: %s\n", status) default: fmt.Println("用法: /tts [on|off|status]") } } func handleTTSCommandSimple(input string) { args := strings.Fields(input) if len(args) == 1 { internal.ToggleTTS() status := "关闭" if internal.IsTTSEnabled() { status = "开启" } fmt.Printf("TTS 已%s\n", status) return } switch args[1] { case "on": internal.SetTTSEnabled(true) fmt.Println("TTS 已开启") case "off": internal.SetTTSEnabled(false) fmt.Println("TTS 已关闭") case "status": status := "关闭" if internal.IsTTSEnabled() { status = "开启" } fmt.Printf("TTS 状态: %s\n", status) default: fmt.Println("用法: /tts [on|off|status]") } }