// Package main 是 hxclaw 应用程序的入口包 // 提供交互式 CLI 界面和流式输出功能 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" ) // Logo 应用 Logo 字符常量 const Logo = "🦐" // main 程序入口函数 // 负责初始化配置、创建 Provider、启动 Agent Loop 和交互式界面 func main() { // 加载 hxclaw 项目配置 if err := internal.LoadProjectConfig(); err != nil { fmt.Fprintf(os.Stderr, "错误:加载项目配置失败: %v\n", err) os.Exit(1) } // 打印应用 Logo 和欢迎信息 logo := internal.GetProjectConfig().UI.Logo fmt.Printf("%s HxClaw - PicoClaw 增强版 CLI\n\n", logo) // 加载 picoclaw 配置文件 cfg, err := internal.LoadConfig() if err != nil { fmt.Fprintf(os.Stderr, "错误:加载配置失败: %v\n", err) os.Exit(1) } // 配置日志系统 logger.ConfigureFromEnv() // 创建 AI Provider(支持 OpenAI、Claude 等) provider, modelID, err := providers.CreateProvider(cfg) if err != nil { fmt.Fprintf(os.Stderr, "错误:创建 Provider 失败: %v\n", err) os.Exit(1) } // 如果命令行指定了模型 ID,覆盖配置文件中的默认模型 if modelID != "" { cfg.Agents.Defaults.ModelName = modelID } // 创建消息总线,用于组件间通信 msgBus := bus.NewMessageBus() defer msgBus.Close() // 创建 Agent Loop,处理用户交互和 AI 请求 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.UserIcon 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.UserIcon) 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 处理请求并以模拟流式方式输出结果 // 1. 显示加载动画表示 AI 正在思考 // 2. 调用 Agent 处理用户输入 // 3. 渲染 Markdown 输出并按行延迟显示 // 4. 打印处理耗时 func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) { startTime := time.Now() // 创建并启动加载动画 spinner := internal.NewSpinner("思考中...") spinner.Start() // 调用 AI 处理用户输入 resp, err := agentLoop.ProcessDirect(context.Background(), input, sessionKey) // 停止加载动画 spinner.Stop() // 处理错误 if err != nil { fmt.Printf("错误: %v\n", err) return } // 渲染 Markdown 并输出 rendered := internal.RenderMarkdown(resp) clearSpinnerLine() outputLineByLine(rendered) // 打印处理耗时 elapsed := time.Since(startTime) printElapsed(elapsed) } // clearSpinnerLine 清除 spinner 行 // 使用终端控制字符清除当前行并移动到行首 func clearSpinnerLine() { output := termenv.DefaultOutput() output.ClearLine() fmt.Print("\r") os.Stdout.Sync() } // outputLineByLine 按行输出文本,模拟打字效果 // 每行之间根据配置延迟,最后一行延迟时间较长 // 空行会直接输出不延迟 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 == "" { lipgloss.Print("\n") continue } lipgloss.Print(line + "\n") // 非最后一行使用普通延迟,最后一行使用较长延迟 if i < totalLines-1 { time.Sleep(lineDelay) } else { time.Sleep(lastLineDelay) } } lipgloss.Print("\n") } // 输出样式定义 var ( iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e")) // 图标样式(金色) textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32")) // 文本样式(深灰) ) // printElapsed 打印处理耗时信息 // 格式化输出:小于 60 秒显示秒数,否则显示分钟数 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) } // formatTokens 格式化 token 数量 // 1000 以上显示为 k 单位(如 1.5k) func formatTokens(n int) string { if n >= 1000 { return fmt.Sprintf("%.1fk", float64(n)/1000) } return fmt.Sprintf("%d", n) } // formatDuration 格式化时长字符串 // 小于 60 秒显示秒数(如 12.5s),否则显示分钟数(如 2.5m) func formatDuration(s float64) string { if s >= 60 { return fmt.Sprintf("%.1fm", s/60) } return fmt.Sprintf("%.1fs", s) }