# hxclaw 更新日志 ## 版本记录 ### v0.1.0 (规划中) - 创建 hxclaw 项目 - 实现流式输出功能 - Markdown 渲染功能(待实现) - 代码高亮功能(待实现) --- ## 待实现功能 ### v0.1.0 (当前) - [x] 流式输出功能 - [x] 导入 picoclaw 核心库 - [x] 实现流式 Provider 调用 - [x] 实时打印 token - [x] 处理非流式 Provider 回退 - [x] 添加加载动画(spinner 组件) - [x] 使用 bubbletea v2 spinner.MiniDot 样式 - [x] 用户输入后显示思考中动画 - [x] 第一个 token 返回后显示思考完成 - [x] 流式输出完成后添加空行分隔 ### v0.2.0 (计划) - [ ] Markdown 渲染 - [ ] Markdown 解析 - [ ] 基础样式(粗体、斜体、链接) - [ ] 代码块渲染 - [ ] 表格渲染 - [ ] 列表渲染 ### 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 模块不需要发布到任何 registry,GitHub 就是事实上的 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" **知识点**:终端输出需要精确控制位置和换行,否则会导致格式错乱。