263 lines
6.5 KiB
Go
263 lines
6.5 KiB
Go
// 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)
|
||
}
|