Files
HxClaw/taolun.md
titor 5d9498f687
Some checks failed
Release / build (push) Failing after 4m28s
feat: 记忆体系统 v0.3.0 完成
## 核心功能
- 双记忆系统合并:picoclaw MEMORY.md + hxclaw 会话摘要
- 独立上下文系统:不依赖 picoclaw session
- 向量检索:硅基流动 BGE-M3 API
- 三重检测:关键词/向量相似度/命令

## 数据库
- libSQL (TursoDB) 存储
- sessions + chats 表设计
- 向量存储使用 binary 编码

## 查询场景
- RecallHistory: 查询所有会话摘要
- RecallTopic: 按话题向量检索
- RecallSession: 指定会话详情
- RecallWithinSession: 会话内检索

## 导出
- MongoDB 风格:~/.config/hxclaw/export-data.json
- chats 嵌套在 sessions 下
- 增量导出,同 session 累加

## UI 优化
- 合并状态显示(耗时 · 状态 · 消息数)
- 颜色设计:金色图标 + 暗绿色/暗红色状态

## 配置项
- memory.recall: keywords, auto_recall, similarity_threshold
- memory.vector: max_search_results
- memory.auto_export
2026-04-27 06:16:19 +08:00

560 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/providersLLM 调用、pkg/config配置加载
### 5. 流式输出原理
- picoclaw 的 providers 已支持 StreamingProvider 接口
- 接口定义ChatStream(ctx, messages, tools, model, options, onChunk)
- onChunk 是回调函数,每个 token 生成时调用
- CLI 层需要判断 provider 是否实现 StreamingProvider然后选择调用
### 6. Markdown 终端渲染
- 使用 charmbracelet 家族库
- glamourMarkdown 渲染(自带代码高亮)
- lipgloss终端样式
- 流程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. 流式输出的实现问题与解决方案
#### 问题 1onChunk 回调接收累积文本
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. 换行控制:
- "思考完成." 后需要两个换行符(一个换行 + 一个空行)
- 流式输出完成后也需要空行分隔
---
### 13. 重绘残留问题与新流程
#### 问题描述
之前的流程:
1. 流式实时打印 token边收边打
2. 完成后 Markdown 重绘
3. 问题:重绘有残留
#### 解决方案:等待完整响应后输出
改进后的流程:
1. AI 返回完整数据 ← 等待时间
2. Markdown 转译
3. 模拟流式输出(从配置读取速度)
效果更好,无残留问题。
#### 配置化
使用 `project.config.yml` 统一管理配置:
```yaml
streaming:
line_delay_ms: 1000 # 每行输出后的延迟(毫秒)
last_line_delay_ms: 600 # 最后一行延迟(毫秒)
markdown:
glamour_style: dark
wrap_width: 0 # 自动获取终端宽度
ui:
logo: "🦐"
user_prefix: "👀 " # 用户输入前缀
```
---
### 14. 按行延迟输出的实现
#### 核心逻辑
```go
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 == "" {
fmt.Println()
continue
}
fmt.Println(line)
if i < totalLines-1 {
time.Sleep(lineDelay)
} else {
time.Sleep(lastLineDelay)
}
}
fmt.Println()
}
```
特点:
- 空行直接跳过
- 每行输出后延迟可配置
- 最后一行延迟可单独配置
---
### 15. 工具调用结果显示问题
#### 问题
使用 `ChatStream` 时,工具调用结果不显示。
#### 原因分析
1. 工具调用结果不在流式响应中返回,而是通过 `bus.PublishOutbound()` 单独发送
2. `ChatStream` 的 `onChunk` 回调只处理文本内容,不处理工具调用
3. 工具调用在 `runTurn` 循环中执行,结果通过消息总线发送
#### 解决方案
回退使用 `ProcessDirect`,因为它会正确处理:
- 工具调用流程
- 工具结果显示
- Markdown 渲染和按行输出
---
### 16. 状态栏优化
#### 改动内容
- 图标:``
- 图标颜色:`#f0c75e`
- 文字颜色:`#2b2e32`
- 内容:只显示耗时,如 `▣ 耗时: 2.3s`
#### 之前 vs 之后
- 之前:`▣ Tokens: 120 · 耗时: 2.3s · 总Tokens: 350`
- 之后:`▣ 耗时: 2.3s`
---
### 17. 项目配置文件详解
#### project.config.yml 结构
```yaml
# hxclaw 项目配置文件
# 模拟流式输出配置
streaming:
line_delay_ms: 1000 # 每行输出后的延迟(毫秒)
last_line_delay_ms: 600 # 最后一行延迟(毫秒)
# Markdown 渲染配置
markdown:
glamour_style: dark # 渲染主题dark, light, dracula, tokyo-night 等
wrap_width: 0 # 自动换行宽度0=自动获取终端宽度)
# UI 配置
ui:
logo: "🦐" # Logo
user_prefix: "👀 " # 用户输入前缀
# TTS 语音配置
tts:
enabled: false # 全局开关(默认关闭)
port: 9876 # daemon 端口
auto: true # AI 回复后自动朗读
```
#### 配置加载优先级
1. 环境变量 `HXCLAW_CONFIG` 指定路径
2. 项目根目录 `project.config.yml`
#### 代码实现
```go
// internal/config.go
type ProjectConfig struct {
Streaming StreamingConfig `yaml:"streaming"`
Markdown MarkdownConfig `yaml:"markdown"`
UI UIConfig `yaml:"ui"`
}
func getConfigPath() string {
if path := os.Getenv("HXCLAW_CONFIG"); path != "" {
return path
}
return filepath.Join(".", "project.config.yml")
}
```
---
### 18. 行业经验参考
#### CLI 动画最佳实践
- 帧率75ms/帧(约 13fps- GitHub Copilot CLI
- Spinner 动画70-120ms - ora 库
- AI 流式输出30-80ms/字符或行
- 总动画时长:控制在 3 秒内 - Copilot CLI 原则
#### 关键结论
- 人眼需要约 30-50ms 才能感知单次视觉变化
- 空白字符不应逐个输出,应批量处理
- 终端宽度 100% 时 Markdown 渲染会显著增加行数和字符数
---
### 19. TTS 语音朗读集成
#### 架构设计
hxclaw 作为 mimo-tts 的客户端,通过 TCP Socket 连接本地 daemon
```
hxclaw (客户端) --TCP:9876--> mimo-tts daemon (服务端)
|
v
API 调用 (mimo-v2.5-tts)
|
v
返回音频文件路径
|
v
afplay 播放
```
#### 配置文件
```yaml
tts:
enabled: false # 全局开关(默认关闭)
port: 9876 # daemon 端口
auto: true # AI 回复后自动朗读
```
#### 命令支持
| 输入 | 行为 |
|------|------|
| `/tts` | 切换 TTS 开关 |
| `/tts on` | 开启 TTS |
| `/tts off` | 关闭 TTS |
| `/tts status` | 显示状态 |
| `T 消息` | 临时开启并发送 |
#### 动态提示符
- 关闭:`👀 `
- 开启:`👀 🔊 `
#### 实现要点
1. TCP 连接:使用 Go 标准库 `net` 包
2. JSON 请求:发送格式 `{"text": "内容"}`
3. 异步朗读:使用 `go func()` 异步调用
4. 静默失败:网络异常只记录警告日志,不阻塞用户
#### 踩坑记录
**ergochat/readline SetPrompt 无返回值**
```go
// 错误
func (r *Readline) SetPrompt(prompt string) error {
return r.rl.SetPrompt(prompt) // SetPrompt 返回 void
}
// 正确
func (r *Readline) SetPrompt(prompt string) {
r.rl.SetPrompt(prompt) // void 类型
}
```
---
### 20. 禁用 picoclaw session 历史,实现独立上下文系统
#### 问题背景
- picoclaw 的 session 历史会被自动清空
- 不利于 hxclaw 的会话连续性
- 需要实现自我控制的上下文系统
#### 解决方案
- 禁用 picoclaw 的 session 历史读取
- 使用 hxclaw 自己的 libSQL 数据库存储会话摘要
- 在 ProcessDirect() 调用前注入上下文摘要到用户输入
#### 实现步骤
1. **创建 GetContextPrompt()** - `cmd/hxclaw/internal/memory/save.go`
```go
func GetContextPrompt() string {
// 从 hxclaw 自己的数据库获取会话摘要
return "=== 当前会话摘要 ===
" + session.Summary + "
============
"
}
```
2. **注入上下文** - `cmd/hxclaw/main.go`
```go
if memoryCfg.Enabled {
contextPrompt := memory.GetContextPrompt()
if contextPrompt != "" {
input = contextPrompt + "
用户新问题: " + input
}
}
resp, err := agentLoop.ProcessDirect(context.Background(), input, sessionKey)
```
#### 效果
- hxclaw 完全独立于 picoclaw session 管理
- 会话摘要通过数据库持久化
- 上下文通过摘要注入传递
---
### 21. UI 合并显示与颜色设计
#### 需求
将原来分两行显示的信息合并为一行:
- 之前:`[memory] 已保存,当前会话 8 条消息` + `▣ 耗时: 20.3s`
- 之后:`▣ 耗时: 20.3s · 会话已保存 · 当前会话 8 条消息`
#### 颜色设计
| 文字 | 颜色 | 十六进制 |
|------|------|---------|
| ▣ (图标) | 金色 | #f0c75e |
| 灰色文字 | 灰色 | #2b2e32 |
| 会话已保存 | 暗绿色 | #4a9e6b |
| 会话保存异常 | 暗红色 | #c75050 |
#### 代码实现
```go
var (
iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e"))
textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32"))
memoryOkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#4a9e6b"))
memoryErrStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#c75050"))
)
func printElapsed(elapsed time.Duration, chatCount int, saveErr error) {
icon := iconStyle.Render("▣ ")
timeText := textStyle.Render(fmt.Sprintf("耗时: %s", elapsedStr))
var statusText string
if saveErr != nil {
statusText = memoryErrStyle.Render("会话保存异常")
} else if chatCount > 0 {
statusText = memoryOkStyle.Render("会话已保存")
}
memCountText := textStyle.Render(fmt.Sprintf("当前会话 %d 条消息", chatCount))
fmt.Printf(" %s%s · %s · %s
", icon, timeText, statusText, memCountText)
}
```
#### 关键点
- `SaveChat()` 改为返回 `(chatCount int, err error)`,便于错误处理
- 状态文字单独使用颜色样式
- 失败时显示"会话保存异常"(暗红色)