feat: 配置系统重构,添加用户配置和中文注释
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// Package internal 包含 hxclaw 的内部工具模块
|
||||
// 提供配置管理、Markdown 渲染、输入读取等功能
|
||||
package internal
|
||||
|
||||
import (
|
||||
@@ -8,105 +10,235 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ProjectConfig struct {
|
||||
// Config 是项目配置结构体,包含流式输出、Markdown 渲染和 UI 相关配置
|
||||
type Config struct {
|
||||
// Streaming 流式输出配置
|
||||
Streaming StreamingConfig `yaml:"streaming"`
|
||||
Markdown MarkdownConfig `yaml:"markdown"`
|
||||
UI UIConfig `yaml:"ui"`
|
||||
// Markdown Markdown 渲染配置
|
||||
Markdown MarkdownConfig `yaml:"markdown"`
|
||||
// UI UI 显示配置
|
||||
UI UIConfig `yaml:"ui"`
|
||||
}
|
||||
|
||||
// StreamingConfig 流式输出配置,控制模拟打字效果的延迟时间
|
||||
type StreamingConfig struct {
|
||||
LineDelayMs int `yaml:"line_delay_ms"`
|
||||
// LineDelayMs 每行输出延迟(毫秒)
|
||||
LineDelayMs int `yaml:"line_delay_ms"`
|
||||
// LastLineDelayMs 最后一行输出延迟(毫秒)
|
||||
LastLineDelayMs int `yaml:"last_line_delay_ms"`
|
||||
}
|
||||
|
||||
// MarkdownConfig Markdown 渲染配置,控制渲染主题和换行行为
|
||||
type MarkdownConfig struct {
|
||||
GlamourStyle string `yaml:"glamour_style"`
|
||||
WrapWidth int `yaml:"wrap_width"`
|
||||
// Theme 渲染主题,支持 dark、light、dracula、tokyo-night 等
|
||||
Theme string `yaml:"theme"`
|
||||
// LineWidth 自动换行宽度(0=自动,>0=固定宽度,-1=禁用)
|
||||
LineWidth int `yaml:"line_width"`
|
||||
}
|
||||
|
||||
// UIConfig UI 显示配置,控制 Logo 和用户提示符
|
||||
type UIConfig struct {
|
||||
Logo string `yaml:"logo"`
|
||||
UserPrefix string `yaml:"user_prefix"`
|
||||
// Logo 应用 Logo 字符
|
||||
Logo string `yaml:"logo"`
|
||||
// UserIcon 用户输入提示符
|
||||
UserIcon string `yaml:"user_icon"`
|
||||
}
|
||||
|
||||
var (
|
||||
defaultCfg = ProjectConfig{
|
||||
// defaultCfg 默认配置值,当配置文件不存在或字段为空时使用
|
||||
defaultCfg = Config{
|
||||
Streaming: StreamingConfig{
|
||||
LineDelayMs: 1000,
|
||||
LastLineDelayMs: 600,
|
||||
},
|
||||
Markdown: MarkdownConfig{
|
||||
GlamourStyle: "dark",
|
||||
WrapWidth: 0,
|
||||
Theme: "dark",
|
||||
LineWidth: 0,
|
||||
},
|
||||
UI: UIConfig{
|
||||
Logo: "🦐",
|
||||
UserPrefix: "👀 ",
|
||||
Logo: "🦐",
|
||||
UserIcon: "👀 ",
|
||||
},
|
||||
}
|
||||
projCfg *ProjectConfig
|
||||
projCfgLock sync.RWMutex
|
||||
cfg *Config // 已合并的配置(用户配置 + 项目配置)
|
||||
cfgLock sync.RWMutex // 配置读写锁
|
||||
userCfg *Config // 用户配置文件解析结果
|
||||
userLock sync.RWMutex // 用户配置读写锁
|
||||
)
|
||||
|
||||
func LoadProjectConfig() error {
|
||||
projCfgLock.Lock()
|
||||
defer projCfgLock.Unlock()
|
||||
// 用户配置文件路径常量
|
||||
const (
|
||||
userConfigDir = ".config/hxclaw" // 用户配置目录(相对于用户家目录)
|
||||
userConfigFile = "config.yml" // 用户配置文件名
|
||||
)
|
||||
|
||||
cfgPath := getConfigPath()
|
||||
if cfgPath == "" {
|
||||
projCfg = &defaultCfg
|
||||
// LoadProjectConfig 加载并合并项目配置
|
||||
// 加载顺序:用户配置(~/.config/hxclaw/config.yml) > 项目配置(project.config.yml)
|
||||
// 最终配置由两者合并得出,用户配置优先于项目配置
|
||||
func LoadProjectConfig() error {
|
||||
cfgLock.Lock()
|
||||
defer cfgLock.Unlock()
|
||||
userLock.Lock()
|
||||
defer userLock.Unlock()
|
||||
|
||||
userCfg = loadUserConfig()
|
||||
projCfg := loadProjectConfig()
|
||||
|
||||
merged := mergeConfig(userCfg, projCfg)
|
||||
cfg = merged
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadUserConfig 加载用户配置文件
|
||||
// 路径:~/.config/hxclaw/config.yml
|
||||
// 如果文件不存在,则自动创建默认配置文件
|
||||
func loadUserConfig() *Config {
|
||||
userPath := getUserConfigPath()
|
||||
if userPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(userPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
createUserConfig(userPath)
|
||||
return &defaultCfg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var userCfg Config
|
||||
if err := yaml.Unmarshal(data, &userCfg); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &userCfg
|
||||
}
|
||||
|
||||
// createUserConfig 创建默认的用户配置文件
|
||||
// userPath: 配置文件完整路径
|
||||
func createUserConfig(userPath string) error {
|
||||
dir := filepath.Dir(userPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultContent := `# hxclaw 用户配置文件
|
||||
# 此文件位于 ~/.config/hxclaw/config.yml
|
||||
|
||||
# Markdown 渲染配置
|
||||
markdown:
|
||||
theme: dark # 渲染主题:dark, light, dracula, tokyo-night 等
|
||||
line_width: 0 # 自动换行宽度(0=自动,>0=固定宽度,-1=禁用)
|
||||
|
||||
# UI 配置
|
||||
ui:
|
||||
logo: "🦐"
|
||||
user_icon: "👀 "
|
||||
`
|
||||
return os.WriteFile(userPath, []byte(defaultContent), 0644)
|
||||
}
|
||||
|
||||
// loadProjectConfig 加载项目级配置文件
|
||||
// 路径优先级:环境变量 HXCLAW_CONFIG 指定路径 > ./project.config.yml
|
||||
func loadProjectConfig() *Config {
|
||||
cfgPath := getProjectConfigPath()
|
||||
if cfgPath == "" {
|
||||
return &defaultCfg
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
projCfg = &defaultCfg
|
||||
return nil
|
||||
return &defaultCfg
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var cfg ProjectConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.Streaming.LineDelayMs <= 0 {
|
||||
cfg.Streaming.LineDelayMs = defaultCfg.Streaming.LineDelayMs
|
||||
}
|
||||
if cfg.Streaming.LastLineDelayMs <= 0 {
|
||||
cfg.Streaming.LastLineDelayMs = defaultCfg.Streaming.LastLineDelayMs
|
||||
}
|
||||
if cfg.Markdown.GlamourStyle == "" {
|
||||
cfg.Markdown.GlamourStyle = defaultCfg.Markdown.GlamourStyle
|
||||
}
|
||||
if cfg.Markdown.WrapWidth < 0 {
|
||||
cfg.Markdown.WrapWidth = 0
|
||||
}
|
||||
if cfg.UI.Logo == "" {
|
||||
cfg.UI.Logo = defaultCfg.UI.Logo
|
||||
}
|
||||
if cfg.UI.UserPrefix == "" {
|
||||
cfg.UI.UserPrefix = defaultCfg.UI.UserPrefix
|
||||
}
|
||||
|
||||
projCfg = &cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetProjectConfig() *ProjectConfig {
|
||||
projCfgLock.RLock()
|
||||
defer projCfgLock.RUnlock()
|
||||
if projCfg == nil {
|
||||
return &defaultCfg
|
||||
}
|
||||
return projCfg
|
||||
|
||||
var projCfg Config
|
||||
if err := yaml.Unmarshal(data, &projCfg); err != nil {
|
||||
return &defaultCfg
|
||||
}
|
||||
|
||||
return &projCfg
|
||||
}
|
||||
|
||||
func getConfigPath() string {
|
||||
// mergeConfig 合并用户配置和项目配置
|
||||
// 合并规则:用户配置优先于项目配置,项目配置优先于默认配置
|
||||
// 仅当配置值非空/非零时才覆盖默认值
|
||||
func mergeConfig(userCfg, projCfg *Config) *Config {
|
||||
result := defaultCfg
|
||||
|
||||
if projCfg != nil {
|
||||
if projCfg.Streaming.LineDelayMs > 0 {
|
||||
result.Streaming.LineDelayMs = projCfg.Streaming.LineDelayMs
|
||||
}
|
||||
if projCfg.Streaming.LastLineDelayMs > 0 {
|
||||
result.Streaming.LastLineDelayMs = projCfg.Streaming.LastLineDelayMs
|
||||
}
|
||||
if projCfg.Markdown.Theme != "" {
|
||||
result.Markdown.Theme = projCfg.Markdown.Theme
|
||||
}
|
||||
if projCfg.Markdown.LineWidth != 0 {
|
||||
result.Markdown.LineWidth = projCfg.Markdown.LineWidth
|
||||
}
|
||||
if projCfg.UI.Logo != "" {
|
||||
result.UI.Logo = projCfg.UI.Logo
|
||||
}
|
||||
if projCfg.UI.UserIcon != "" {
|
||||
result.UI.UserIcon = projCfg.UI.UserIcon
|
||||
}
|
||||
}
|
||||
|
||||
if userCfg != nil {
|
||||
if userCfg.Markdown.Theme != "" {
|
||||
result.Markdown.Theme = userCfg.Markdown.Theme
|
||||
}
|
||||
if userCfg.Markdown.LineWidth != 0 {
|
||||
result.Markdown.LineWidth = userCfg.Markdown.LineWidth
|
||||
}
|
||||
if userCfg.UI.Logo != "" {
|
||||
result.UI.Logo = userCfg.UI.Logo
|
||||
}
|
||||
if userCfg.UI.UserIcon != "" {
|
||||
result.UI.UserIcon = userCfg.UI.UserIcon
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// GetProjectConfig 获取当前生效的配置(线程安全)
|
||||
// 如果配置未加载,返回默认配置
|
||||
func GetProjectConfig() *Config {
|
||||
cfgLock.RLock()
|
||||
defer cfgLock.RUnlock()
|
||||
if cfg == nil {
|
||||
return &defaultCfg
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// getUserConfigPath 获取用户配置文件路径
|
||||
// 路径格式:~/.config/hxclaw/config.yml
|
||||
// 支持 Windows (USERPROFILE) 和 Unix (HOME) 环境变量
|
||||
func getUserConfigPath() string {
|
||||
homeDir := os.Getenv("USERPROFILE")
|
||||
if homeDir == "" {
|
||||
homeDir = os.Getenv("HOME")
|
||||
}
|
||||
if homeDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(homeDir, userConfigDir, userConfigFile)
|
||||
}
|
||||
|
||||
// getProjectConfigPath 获取项目配置文件路径
|
||||
// 优先使用环境变量 HXCLAW_CONFIG 指定的路径,否则使用当前目录的 project.config.yml
|
||||
func getProjectConfigPath() string {
|
||||
if path := os.Getenv("HXCLAW_CONFIG"); path != "" {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(".", "project.config.yml")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package internal 包含 hxclaw 的内部工具模块
|
||||
// 提供配置管理、Markdown 渲染、输入读取等功能
|
||||
package internal
|
||||
|
||||
import (
|
||||
@@ -9,6 +11,8 @@ import (
|
||||
"github.com/charmbracelet/x/term"
|
||||
)
|
||||
|
||||
// RenderMarkdown 将 Markdown 文本渲染为终端友好的格式
|
||||
// 支持通过配置或环境变量指定渲染主题和换行宽度
|
||||
func RenderMarkdown(md string) string {
|
||||
if md == "" {
|
||||
return ""
|
||||
@@ -67,8 +71,8 @@ func RenderParagraph(text string) string {
|
||||
|
||||
func getStyle() string {
|
||||
if cfg := GetProjectConfig(); cfg != nil {
|
||||
if cfg.Markdown.GlamourStyle != "" {
|
||||
return cfg.Markdown.GlamourStyle
|
||||
if cfg.Markdown.Theme != "" {
|
||||
return cfg.Markdown.Theme
|
||||
}
|
||||
}
|
||||
if s := os.Getenv("GLAMOUR_STYLE"); s != "" {
|
||||
@@ -79,10 +83,10 @@ func getStyle() string {
|
||||
|
||||
func getWrapWidth() int {
|
||||
if cfg := GetProjectConfig(); cfg != nil {
|
||||
if cfg.Markdown.WrapWidth > 0 {
|
||||
return cfg.Markdown.WrapWidth
|
||||
if cfg.Markdown.LineWidth > 0 {
|
||||
return cfg.Markdown.LineWidth
|
||||
}
|
||||
if cfg.Markdown.WrapWidth < 0 {
|
||||
if cfg.Markdown.LineWidth < 0 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package internal 包含 hxclaw 的内部工具模块
|
||||
// 提供配置管理、Markdown 渲染、输入读取等功能
|
||||
package internal
|
||||
|
||||
import (
|
||||
@@ -9,22 +11,26 @@ import (
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
// SpinnerState 表示加载动画的状态
|
||||
type SpinnerState int
|
||||
|
||||
const (
|
||||
StateThinking SpinnerState = iota
|
||||
StateAnswering
|
||||
StateDone
|
||||
StateThinking SpinnerState = iota // 思考中状态
|
||||
StateAnswering // 回答中状态
|
||||
StateDone // 完成状态
|
||||
)
|
||||
|
||||
// Spinner 加载动画组件,用于显示思考状态
|
||||
type Spinner struct {
|
||||
text string
|
||||
state SpinnerState
|
||||
spinner spinner.Model
|
||||
stopCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
text string // 显示的文本内容
|
||||
state SpinnerState // 当前状态
|
||||
spinner spinner.Model // 底层动画模型
|
||||
stopCh chan struct{} // 停止信号通道
|
||||
doneCh chan struct{} // 完成信号通道
|
||||
}
|
||||
|
||||
// NewSpinner 创建一个新的加载动画实例
|
||||
// text: 初始显示的文本内容
|
||||
func NewSpinner(text string) *Spinner {
|
||||
s := spinner.New(
|
||||
spinner.WithSpinner(spinner.MiniDot),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package main 是 hxclaw 应用程序的入口包
|
||||
// 提供交互式 CLI 界面和流式输出功能
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -18,41 +20,53 @@ import (
|
||||
"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{
|
||||
@@ -61,12 +75,15 @@ func main() {
|
||||
"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
|
||||
prompt := internal.GetProjectConfig().UI.UserIcon
|
||||
|
||||
rl, err := internal.NewReadline(prompt)
|
||||
if err != nil {
|
||||
@@ -105,7 +122,7 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
reader := internal.NewSimpleReader()
|
||||
for {
|
||||
fmt.Print(internal.GetProjectConfig().UI.UserPrefix)
|
||||
fmt.Print(internal.GetProjectConfig().UI.UserIcon)
|
||||
line, err := reader.ReadString()
|
||||
if err != nil {
|
||||
if err == internal.ErrEOF {
|
||||
@@ -130,30 +147,42 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
}
|
||||
}
|
||||
|
||||
// runWithStreaming 使用 ProcessDirect 处理请求,支持工具调用和结果显示
|
||||
// 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()
|
||||
@@ -161,6 +190,9 @@ func clearSpinnerLine() {
|
||||
os.Stdout.Sync()
|
||||
}
|
||||
|
||||
// outputLineByLine 按行输出文本,模拟打字效果
|
||||
// 每行之间根据配置延迟,最后一行延迟时间较长
|
||||
// 空行会直接输出不延迟
|
||||
func outputLineByLine(text string) {
|
||||
if text == "" {
|
||||
return
|
||||
@@ -169,10 +201,12 @@ func outputLineByLine(text string) {
|
||||
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")
|
||||
@@ -181,6 +215,7 @@ func outputLineByLine(text string) {
|
||||
|
||||
lipgloss.Print(line + "\n")
|
||||
|
||||
// 非最后一行使用普通延迟,最后一行使用较长延迟
|
||||
if i < totalLines-1 {
|
||||
time.Sleep(lineDelay)
|
||||
} else {
|
||||
@@ -191,11 +226,14 @@ func outputLineByLine(text string) {
|
||||
lipgloss.Print("\n")
|
||||
}
|
||||
|
||||
// 输出样式定义
|
||||
var (
|
||||
iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e"))
|
||||
textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32"))
|
||||
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)
|
||||
@@ -205,6 +243,8 @@ func printElapsed(elapsed time.Duration) {
|
||||
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)
|
||||
@@ -212,6 +252,8 @@ func formatTokens(n int) string {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user