feat: 配置系统重构,添加用户配置和中文注释
This commit is contained in:
249
changelog.md
249
changelog.md
@@ -2,58 +2,26 @@
|
||||
|
||||
## 版本记录
|
||||
|
||||
### v0.1.0 (规划中)
|
||||
### v0.1.0
|
||||
|
||||
- 创建 hxclaw 项目
|
||||
- 实现流式输出功能
|
||||
- Markdown 渲染功能(待实现)
|
||||
- 代码高亮功能(待实现)
|
||||
- [x] 流式输出功能
|
||||
- [x] Markdown 渲染功能
|
||||
- [x] 配置系统(支持用户配置和项目配置)
|
||||
- [x] 代码中文注释
|
||||
|
||||
---
|
||||
|
||||
## 待实现功能
|
||||
|
||||
### 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 加载动画组件
|
||||
- [ ] 集成 glow 或类似库
|
||||
- [ ] 支持常见语言语法高亮
|
||||
|
||||
---
|
||||
|
||||
@@ -85,14 +53,7 @@
|
||||
|
||||
**问题**:不是所有 Provider 都支持流式输出
|
||||
|
||||
**纠正**:需要使用类型断言判断 Provider 是否实现 `providers.StreamingProvider` 接口:
|
||||
```go
|
||||
if sp, ok := provider.(providers.StreamingProvider); ok {
|
||||
// 使用 ChatStream
|
||||
} else {
|
||||
// 使用普通 Chat
|
||||
}
|
||||
```
|
||||
**纠正**:需要使用类型断言判断 Provider 是否实现 `providers.StreamingProvider` 接口
|
||||
|
||||
**知识点**:picoclaw 的 Provider 设计使用了接口分离原则,流式是可选能力
|
||||
|
||||
@@ -103,8 +64,8 @@ if sp, ok := provider.(providers.StreamingProvider); ok {
|
||||
**问题**:如何实现 Markdown 终端渲染
|
||||
|
||||
**纠正**:使用 charmbracelet 家族:
|
||||
- glamour:Markdown 渲染
|
||||
- lipgloss:样式定义
|
||||
- glow:代码高亮
|
||||
|
||||
**知识点**:charmbracelet 是 Go 终端UI 的事实标准,API 设计优雅
|
||||
|
||||
@@ -122,133 +83,16 @@ if sp, ok := provider.(providers.StreamingProvider); ok {
|
||||
|
||||
---
|
||||
|
||||
### 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)
|
||||
```
|
||||
**问题**:picoclaw 的 `StreamingProvider` 接口定义 `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)
|
||||
}
|
||||
}
|
||||
```
|
||||
**纠正**:使用 `printedLen` 跟踪已打印位置,只打印新增部分
|
||||
|
||||
**知识点**: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 动画不动
|
||||
@@ -257,21 +101,7 @@ os.Stdout.Sync()
|
||||
|
||||
**纠正**: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) // 必须使用返回值更新
|
||||
}
|
||||
}
|
||||
s.spinner, _ = s.spinner.Update(msg) // 必须使用返回值更新
|
||||
```
|
||||
|
||||
**知识点**:bubbletea v2 的组件遵循 TEA 架构模式,Update 方法返回更新后的 model,需要显式使用返回值。
|
||||
@@ -282,36 +112,37 @@ func (s *Spinner) tick() {
|
||||
|
||||
**问题**:spinner 使用 `\r` 回到行首刷新,流式输出也在同一行打印,导致内容混在一起
|
||||
|
||||
**现象**:
|
||||
```
|
||||
⠋ 回答中... 好
|
||||
⠋ 回答中... 注于
|
||||
```
|
||||
|
||||
**纠正**:在第一个 token 时停止 spinner,让 spinner 输出 "思考完成." 并换行,然后再开始流式打印:
|
||||
```go
|
||||
if firstToken && len(accumulated) > 0 {
|
||||
spinner.Stop() // 停止 spinner,会打印 "思考完成."
|
||||
firstToken = false
|
||||
}
|
||||
```
|
||||
**纠正**:在第一个 token 时停止 spinner,让 spinner 输出 "思考完成." 并换行,然后再开始流式打印
|
||||
|
||||
**知识点**:spinner 和流式输出需要分时工作,不能同时占用同一行。
|
||||
|
||||
---
|
||||
|
||||
### spinner 动画位置和换行策略
|
||||
### 配置系统设计
|
||||
|
||||
**问题**:用户期望动画在前,文字在后,且需要正确的换行
|
||||
**问题**:用户需要自定义主题、延迟等配置,但项目配置只有一份
|
||||
|
||||
**效果**:
|
||||
```
|
||||
思考中... ⠋ -> 用户期望 ⠋ 思考中...
|
||||
思考完成. -> 用户期望 ⠋ 思考完成.
|
||||
```
|
||||
**纠正**:设计多级配置系统:
|
||||
- 用户配置:`~/.config/hxclaw/config.yml`(优先级最高)
|
||||
- 项目配置:`project.config.yml`
|
||||
- 环境变量:`HXCLAW_CONFIG` 指定路径
|
||||
- 代码默认值
|
||||
|
||||
**纠正**:
|
||||
- 动画在前,文字在后:`fmt.Printf("\r%s %s", s.spinner.View(), s.text)`
|
||||
- 换行:"思考完成.\n" + 流式输出后 "fmt.Println()\n"
|
||||
**合并规则**:用户配置优先于项目配置,项目配置优先于默认值
|
||||
|
||||
**知识点**:终端输出需要精确控制位置和换行,否则会导致格式错乱。
|
||||
**实现**:`mergeConfig` 函数实现配置合并逻辑
|
||||
|
||||
---
|
||||
|
||||
### 代码注释规范
|
||||
|
||||
**问题**:代码缺少注释,后续维护困难
|
||||
|
||||
**纠正**:所有代码添加详细中文注释:
|
||||
- 包级别注释
|
||||
- 结构体注释
|
||||
- 字段注释(使用行内注释 `//`)
|
||||
- 函数注释
|
||||
- 关键逻辑注释
|
||||
|
||||
**知识点**:详细注释是团队协作和后续维护的基础
|
||||
Reference in New Issue
Block a user