8.5 KiB
hxclaw 更新日志
版本记录
v0.2.0
- 新增 TTS 语音朗读功能
- 集成 mimo-tts client 功能,通过 TCP 连接本地 daemon
- 支持配置文件开关(tts.enabled)
- 支持命令行切换(/tts on/off/status)
- 支持临时 TTS 前缀(
T 消息临时开启) - 动态提示符显示 TTS 状态(👀 🔊)
- 静默失败处理(网络异常时警告日志)
v0.1.0
- 创建 hxclaw 项目
- 实现流式输出功能
- Markdown 渲染功能(glamour)
- Markdown(glamour)
- 项目配置
待实现功能
v0.2.0 (当前)
- TTS 语音朗读功能
- 集成 mimo-tts client (TCP 连接)
- 配置文件开关 (tts.enabled)
- 命令行切换 (/tts on/off/status)
- 临时 TTS 前缀 (T 消息)
- 动态提示符显示状态
- 静默失败处理
v0.3.0 (计划)
- 命令行参数支持(--theme, --tts 等)
- 多语言支持
- 会话历史持久化
目前进度
- 创建项目目录结构
- 编写讨论记录(taolun.md)
- 编写更新日志(changelog.md)
- 编写 AI 行为指南(agents.md)
- 创建 go.mod
- 实现 main.go 入口
- 实现流式输出核心逻辑
- 编译成功,生成 hxclaw 二进制
- 添加 spinner 加载动画组件
认知纠正(踩坑记录)
Go replace 机制不需要发布到 registry
问题:最初担心需要像 npm 那样发布到 registry 才能被其他项目引用
纠正:Go 的 replace 机制可以直接指向:
- 本地路径(如
../picoclaw) - GitHub 仓库 + tag(如
github.com/sipeed/picoclaw v0.2.4)
知识点:Go 模块不需要发布到任何 registry,GitHub 就是事实上的 registry
hxclaw 不需要实现全部 picoclaw 功能
问题:最初担心需要自己实现 onboard、tools、mcp 等全部功能
纠正:hxclaw 是 CLI 增强层,只替换交互逻辑。picoclaw 的核心功能(agent loop、tools、mcp、skills)通过导入其 pkg 即可复用
知识点:采用组合优于继承的设计,需要什么功能就导入对应的包
流式输出需要判断 Provider 是否支持
问题:不是所有 Provider 都支持流式输出
纠正:需要使用类型断言判断 Provider 是否实现 providers.StreamingProvider 接口:
if sp, ok := provider.(providers.StreamingProvider); ok {
// 使用 ChatStream
} else {
// 使用普通 Chat
}
知识点:picoclaw 的 Provider 设计使用了接口分离原则,流式是可选能力
终端渲染使用 charmbracelet 库
问题:如何实现 Markdown 终端渲染
纠正:使用 charmbracelet 家族:
- lipgloss:样式定义
- glow:代码高亮
知识点:charmbracelet 是 Go 终端UI 的事实标准,API 设计优雅
独立二进制部署方式
问题:hxclaw 和 picoclaw 的关系
纠正:hxclaw 作为独立二进制,用户可以同时保留两个命令:
picoclaw agent使用原版hxclaw使用增强版
知识点:通过 go.mod replace 实现依赖绑定,用户无需安装 picoclaw 源码
AgentRegistry 没有 BuildMessages 方法
问题:最初尝试调用 agentLoop.GetRegistry().BuildMessages() 构建消息
纠正:BuildMessages 属于 ContextBuilder,不是 AgentRegistry:
// 正确方式
agentInstance.ContextBuilder.BuildMessages(history, summary, input, media, channel, chatID, senderID, senderDisplayName)
知识点:picoclaw 代码结构中,ContextBuilder 负责消息构建,AgentRegistry 负责 agent 管理
ToolDefinitions 获取方式
问题:如何获取可用的工具定义列表
纠正:通过 ToolRegistry 的 ToProviderDefs 方法:
toolDefs := agentInstance.Tools.ToProviderDefs()
知识点:ToolRegistry 维护工具注册,ToProviderDefs 转换为 provider 可用的格式
流式输出实时刷新
问题:流式输出时字符不是实时显示,要等很久才一次性出现
纠正:在 onChunk 回调中添加 os.Stdout.Sync() 强制刷新 stdout:
func(token string) {
fmt.Print(token)
os.Stdout.Sync() // 强制刷新
}
知识点:Go 的 fmt.Print 使用缓冲输出,需要手动刷新才能实时显示
Session 历史消息获取
问题:如何获取会话历史用于流式调用
纠正:通过 SessionStore 接口:
history := agentInstance.Sessions.GetHistory(sessionKey)
summary := agentInstance.Sessions.GetSummary(sessionKey)
知识点:AgentInstance.Sessions 实现了 SessionStore 接口,支持 GetHistory 和 GetSummary 方法
流式调用后的消息保存
问题:流式调用绕过了 agent loop,消息没有保存到 session
纠正:流式调用后手动保存消息:
agentInstance.Sessions.AddMessage(sessionKey, "user", input)
agentInstance.Sessions.AddMessage(sessionKey, "assistant", result)
知识点:SessionStore 接口提供 AddMessage 方法,支持 "user" 和 "assistant" 角色
onChunk 回调接收累积文本导致重复输出
问题:picoclaw 的 StreamingProvider 接口定义:
onChunk func(accumulated string)
注释说明:"onChunk receives the accumulated text so far (not individual deltas)"。每次回调时参数是累积的完整文本(如 "你好" → "你好!再次" → "你好!再次见到"),而不是增量。
纠正:使用 printedLen 跟踪已打印位置,只打印新增部分:
var printedLen int
func(accumulated string) {
if len(accumulated) > printedLen {
fmt.Print(accumulated[printedLen:])
printedLen = len(accumulated)
}
}
知识点:picoclaw 故意设计为累积文本,这样可以在任意时刻获取完整内容用于调试
尝试 uilive 库但只显示最后一行
问题:为了实现同行流动效果,尝试使用 github.com/gosuri/uilive 库
现象:该库会覆盖每一行,只显示最后一行内容
纠正:移除 uilive,直接使用 fmt.Print + os.Stdout.Sync(),让终端自然处理换行
知识点:uilive 适用于进度条等场景,不适合长文本流式输出
流式输出期望同行流动但实际换行显示
问题:用户期望像 ollama 那样在同行逐字符流动
最终方案:
fmt.Print(accumulated[printedLen:])
os.Stdout.Sync()
效果:
- 字符串自然累积增长
- 终端自动处理换行(满一行自动 wrap)
- 保留所有历史输出
- 每次刷新缓冲区确保立即显示
知识点:最简单的方案就是最有效的方案,不需要额外库
spinner 组件的 model 更新必须使用返回值
问题:spinner 动画不动
现象:调用 spinner.Update(msg) 后动画不更新
纠正:spinner model 是值类型,需要使用返回值更新:
// 错误写法
func (s *Spinner) tick() {
msg := s.spinner.Tick()
if msg, ok := msg.(spinner.TickMsg); ok {
s.spinner.Update(msg) // 动画不会动!
}
}
// 正确写法
func (s *Spinner) tick() {
msg := s.spinner.Tick()
if msg, ok := msg.(spinner.TickMsg); ok {
s.spinner, _ = s.spinner.Update(msg) // 必须使用返回值更新
}
}
知识点:bubbletea v2 的组件遵循 TEA 架构模式,Update 方法返回更新后的 model,需要显式使用返回值。
spinner 和流式输出在同一行的冲突问题
问题:spinner 使用 \r 回到行首刷新,流式输出也在同一行打印,导致内容混在一起
现象:
⠋ 回答中... 好
⠋ 回答中... 注于
纠正:在第一个 token 时停止 spinner,让 spinner 输出 "思考完成." 并换行,然后再开始流式打印:
if firstToken && len(accumulated) > 0 {
spinner.Stop() // 停止 spinner,会打印 "思考完成."
firstToken = false
}
知识点:spinner 和流式输出需要分时工作,不能同时占用同一行。
spinner 动画位置和换行策略
问题:用户期望动画在前,文字在后,且需要正确的换行
效果:
思考中... ⠋ -> 用户期望 ⠋ 思考中...
思考完成. -> 用户期望 ⠋ 思考完成.
纠正:
- 动画在前,文字在后:
fmt.Printf("\r%s %s", s.spinner.View(), s.text) - 换行:"思考完成.\n" + 流式输出后 "fmt.Println()\n"
知识点:终端输出需要精确控制位置和换行,否则会导致格式错乱。