# hxclaw 讨论记录 ## 知识点汇总 ### 1. hxclaw 是什么? - hxclaw 是基于 picoclaw 的 CLI 增强工具 - 提供流式输出(替代原有的批量输出) - 提供 Markdown 终端渲染(提升阅读体验) - 定位: picoclaw 的变体,面向需要更好终端交互体验的用户 ### 2. 为什么采用独立二进制而非插件? - picoclaw 目前没有正式的插件系统 - 工具扩展通过 ToolRegistry 注册(如 spi.go, i2c.go) - 技能通过 SKILL.md 文件加载 - MCP 通过配置连接外部服务器 - 最小侵入方式:独立二进制 + 复用核心库 ### 3. 如何复用 picoclaw 功能? - 使用 Go 的 replace 机制在 go.mod 中声明依赖 - 开发时 replace 指向本地 picoclaw 目录 - 发布时 replace 指向 GitHub 具体版本(如 v0.2.4) - hxclaw 只需导入:pkg/agent, pkg/providers, pkg/config, pkg/bus ### 4. hxclaw 架构设计 ``` hxclaw/ ├── cmd/hxclaw/ # CLI 入口(自己实现) ├── go.mod # 依赖配置 └── pkg/ # 空目录,全量复用 picoclaw ``` - 不依赖:pkg/channels(不需要消息通道)、pkg/gateway(不需要 HTTP 服务)、web/(不需要网页) - 依赖:pkg/agent(核心逻辑)、pkg/providers(LLM 调用)、pkg/config(配置加载) ### 5. 流式输出原理 - picoclaw 的 providers 已支持 StreamingProvider 接口 - 接口定义:ChatStream(ctx, messages, tools, model, options, onChunk) - onChunk 是回调函数,每个 token 生成时调用 - CLI 层需要判断 provider 是否实现 StreamingProvider,然后选择调用 ### 6. Markdown 终端渲染 - 使用 charmbracelet 家族库 - lipgloss:终端样式 - glow:代码高亮 - 流程:Markdown → ANSI 转义序列 → 终端显示 ### 7. 部署方式 - 独立二进制 hxclaw,与 picoclaw 二进制共存于同一目录 - 用户使用 `hxclaw` 命令调用增强版 CLI - 配置文件复用 picoclaw 的 config.json(位于 ~/.picoclaw/config.json) ### 8. 版本同步策略 - 关键版本跟进(功能大版本更新时) - 不需要每次 picoclaw 升级都同步 - 依赖版本在 go.mod 中声明,更新时修改 replace 目标版本即可 ### 9. AgentRegistry 的正确使用方式 - `AgentRegistry` 负责管理多个 Agent 实例 - `GetDefaultAgent()` 获取默认的 Agent 实例 - `AgentInstance` 包含: - `Provider` - LLM 提供者 - `ContextBuilder` - 消息构建器 - `Tools` - 工具注册表 - `Model` - 模型名称 - 注意:`AgentRegistry` 没有 `BuildMessages` 方法,该方法属于 `ContextBuilder` ### 10. ToolDefinitions 的获取方式 - 通过 `agentInstance.Tools.ToProviderDefs()` 获取 - 返回 `[]providers.ToolDefinition` 格式 - 该方法将工具注册表中的工具转换为 provider 可用的格式 ### 11. 流式输出的实现问题与解决方案 #### 问题 1:onChunk 回调接收累积文本 picoclaw 的 `StreamingProvider` 接口定义: ```go onChunk func(accumulated string) ``` 注释明确说明:"onChunk receives the accumulated text so far (not individual deltas)"。 这意味着每次回调时参数是完整的累积文本(如 "你好" → "你好!再次" → "你好!再次见到"),而不是增量。 #### 问题 2:直接打印导致重复输出 如果直接打印 token: ```go func(token string) { fmt.Print(token) // 打印累积文本! } ``` 会导致输出: ``` 你好 你好!再次 你好!再次见到 ... ``` #### 解决方案 1:跟踪已打印长度 使用 `printedLen` 跟踪已打印的字符位置,只打印新增部分: ```go var printedLen int func(accumulated string) { if len(accumulated) > printedLen { fmt.Print(accumulated[printedLen:]) printedLen = len(accumulated) } } ``` #### 问题 3:尝试使用 uilive 库 尝试使用 `github.com/gosuri/uilive` 库实现同行流动效果,但发现该库会覆盖每一行,只显示最后一行内容,不符合需求。 #### 最终解决方案:直接 Print + Sync ```go fmt.Print(accumulated[printedLen:]) os.Stdout.Sync() ``` 这样: 1. 字符串自然累积增长 2. 终端自动处理换行(满一行自动 wrap) 3. 保留所有历史输出 4. 每次刷新缓冲区确保立即显示 这正是 ollama 等工具的流式输出效果。 --- ### 12. 使用 bubbletea v2 的 spinner 组件实现加载动画 #### 需求分析 用户希望在使用流式输出时,显示加载动画: - 用户输入后显示 "思考中... ⠋" - 第一个 token 返回后显示 "思考完成." - 流式输出完成后添加空行分隔 #### 技术选型 使用 `charm.land/bubbles/v2/spinner` 组件,这是 bubbletea v2 官方提供的 spinner 组件。 #### 实现方案 创建独立的 Spinner 结构体,在独立 goroutine 中运行动画: ```go type Spinner struct { text string state SpinnerState spinner spinner.Model stopCh chan struct{} doneCh chan struct{} } ``` 关键点: - 使用 `spinner.MiniDot` 动画样式 - 独立 goroutine 使用 ticker 驱动动画帧切换 - 使用 `\r` 回车符在同一行刷新动画 - Stop 时输出 "思考完成." #### 官方示例参考 ```go func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd } } ``` 关键:spinner.Tick() 返回的 TickMsg 需要传给 spinner.Update(),并使用返回值更新 spinner model。 #### 注意事项 1. spinner model 更新必须使用返回值: ```go s.spinner, _ = s.spinner.Update(msg) // 正确 s.spinner.Update(msg) // 错误!动画不会动 ``` 2. 动画位置:动画在前,文字在后: ```go fmt.Printf("\r%s %s", s.spinner.View(), s.text) // ⠋ 思考中... ``` 3. 换行控制: - "思考完成." 后需要两个换行符(一个换行 + 一个空行) - 流式输出完成后也需要空行分隔