Files
HxClaw/changelog.md

317 lines
8.5 KiB
Markdown
Raw Normal View History

# 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
- 代码高亮功能(待实现)
---
## 待实现功能
### v0.2.0 (当前)
- [x] TTS 语音朗读功能
- [x] 集成 mimo-tts client (TCP 连接)
- [x] 配置文件开关 (tts.enabled)
- [x] 命令行切换 (/tts on/off/status)
- [x] 临时 TTS 前缀 (T 消息)
- [x] 动态提示符显示状态
- [x] 静默失败处理
### v0.3.0 (计划)
- [ ] 代码高亮
- [ ] 集成 glow 或类似库
- [ ] 支持常见语言语法高亮
---
## 目前进度
- [x] 创建项目目录结构
- [x] 编写讨论记录taolun.md
- [x] 编写更新日志changelog.md
- [x] 编写 AI 行为指南agents.md
- [x] 创建 go.mod
- [x] 实现 main.go 入口
- [x] 实现流式输出核心逻辑
- [x] 编译成功,生成 hxclaw 二进制
- [x] 添加 spinner 加载动画组件
---
## 认知纠正(踩坑记录)
### Go replace 机制不需要发布到 registry
**问题**:最初担心需要像 npm 那样发布到 registry 才能被其他项目引用
**纠正**Go 的 replace 机制可以直接指向:
- 本地路径(如 `../picoclaw`
- GitHub 仓库 + tag`github.com/sipeed/picoclaw v0.2.4`
**知识点**Go 模块不需要发布到任何 registryGitHub 就是事实上的 registry
---
### hxclaw 不需要实现全部 picoclaw 功能
**问题**:最初担心需要自己实现 onboard、tools、mcp 等全部功能
**纠正**hxclaw 是 CLI 增强层只替换交互逻辑。picoclaw 的核心功能agent loop、tools、mcp、skills通过导入其 pkg 即可复用
**知识点**:采用组合优于继承的设计,需要什么功能就导入对应的包
---
### 流式输出需要判断 Provider 是否支持
**问题**:不是所有 Provider 都支持流式输出
**纠正**:需要使用类型断言判断 Provider 是否实现 `providers.StreamingProvider` 接口:
```go
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
```go
// 正确方式
agentInstance.ContextBuilder.BuildMessages(history, summary, input, media, channel, chatID, senderID, senderDisplayName)
```
**知识点**picoclaw 代码结构中ContextBuilder 负责消息构建AgentRegistry 负责 agent 管理
---
### ToolDefinitions 获取方式
**问题**:如何获取可用的工具定义列表
**纠正**:通过 ToolRegistry 的 ToProviderDefs 方法:
```go
toolDefs := agentInstance.Tools.ToProviderDefs()
```
**知识点**ToolRegistry 维护工具注册ToProviderDefs 转换为 provider 可用的格式
---
### 流式输出实时刷新
**问题**:流式输出时字符不是实时显示,要等很久才一次性出现
**纠正**:在 onChunk 回调中添加 `os.Stdout.Sync()` 强制刷新 stdout
```go
func(token string) {
fmt.Print(token)
os.Stdout.Sync() // 强制刷新
}
```
**知识点**Go 的 `fmt.Print` 使用缓冲输出,需要手动刷新才能实时显示
---
### Session 历史消息获取
**问题**:如何获取会话历史用于流式调用
**纠正**:通过 `SessionStore` 接口:
```go
history := agentInstance.Sessions.GetHistory(sessionKey)
summary := agentInstance.Sessions.GetSummary(sessionKey)
```
**知识点**`AgentInstance.Sessions` 实现了 `SessionStore` 接口,支持 `GetHistory``GetSummary` 方法
---
### 流式调用后的消息保存
**问题**:流式调用绕过了 agent loop消息没有保存到 session
**纠正**:流式调用后手动保存消息:
```go
agentInstance.Sessions.AddMessage(sessionKey, "user", input)
agentInstance.Sessions.AddMessage(sessionKey, "assistant", result)
```
**知识点**`SessionStore` 接口提供 `AddMessage` 方法,支持 "user" 和 "assistant" 角色
---
### onChunk 回调接收累积文本导致重复输出
**问题**picoclaw 的 `StreamingProvider` 接口定义:
```go
onChunk func(accumulated string)
```
注释说明:"onChunk receives the accumulated text so far (not individual deltas)"。每次回调时参数是累积的完整文本(如 "你好" → "你好!再次" → "你好!再次见到"),而不是增量。
**纠正**:使用 `printedLen` 跟踪已打印位置,只打印新增部分:
```go
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 那样在同行逐字符流动
**最终方案**
```go
fmt.Print(accumulated[printedLen:])
os.Stdout.Sync()
```
效果:
- 字符串自然累积增长
- 终端自动处理换行(满一行自动 wrap
- 保留所有历史输出
- 每次刷新缓冲区确保立即显示
**知识点**:最简单的方案就是最有效的方案,不需要额外库
---
### spinner 组件的 model 更新必须使用返回值
**问题**spinner 动画不动
**现象**:调用 spinner.Update(msg) 后动画不更新
**纠正**spinner model 是值类型,需要使用返回值更新:
```go
// 错误写法
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 输出 "思考完成." 并换行,然后再开始流式打印:
```go
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"
**知识点**:终端输出需要精确控制位置和换行,否则会导致格式错乱。