- 恢复e070461修复:wrap_width=-1 禁用换行,使用 lipgloss.Print - 应用dd3c8a0配置重构:字段名变更(theme/line_width/user_icon) - 添加用户配置文件支持(~/.config/hxclaw/config.yml) - 添加 TTS 配置到用户配置(enabled, auto)
334 lines
7.3 KiB
Go
334 lines
7.3 KiB
Go
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.UserIcon
|
|
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.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
|
|
}
|
|
|
|
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 == "" {
|
|
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"))
|
|
)
|
|
|
|
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]")
|
|
}
|
|
}
|