Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c6bbe04f1 | |||
| c7767edd4f | |||
| cc9b2a5c76 | |||
| d27771e020 | |||
| efa10cb0e6 | |||
| 1da98957c2 | |||
| bd48c305ab | |||
| 9653260567 | |||
| 6595a39abe | |||
| 5fd58e44bb | |||
| 38b957d40b | |||
| 85e710ea02 | |||
| 3d9b2d6a90 | |||
| 0427274994 | |||
| c79977496d | |||
| b8d0530118 | |||
| e0b7ea984f | |||
| 1a76243f8f | |||
| dd3c8a03e1 | |||
| e070461fe4 |
@@ -13,19 +13,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GOPROXY: "https://mirrors.aliyun.com/goproxy/,direct"
|
GOPROXY: "https://mirrors.aliyun.com/goproxy/,direct"
|
||||||
steps:
|
steps:
|
||||||
- name: Setup mirrors
|
|
||||||
run: |
|
|
||||||
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: |
|
run: |
|
||||||
|
# 使用阿里云 apk 镜像源
|
||||||
|
echo -e 'https://mirrors.aliyun.com/alpine/v3.20/main/\nhttps://mirrors.aliyun.com/alpine/v3.20/community/' > /etc/apk/repositories
|
||||||
apk add git bash
|
apk add git bash
|
||||||
rm -rf /workspace/hxclaw/hxclaw 2>/dev/null || true
|
git clone https://hub.gaomia.site/titor/HxClaw.git /workspace/hxclaw
|
||||||
git clone https://hub.gaomia.site/titor/hxclaw.git /workspace/hxclaw/hxclaw
|
cp -r /workspace/hxclaw/* /workspace/hxclaw/
|
||||||
cd /workspace/hxclaw/hxclaw
|
cp -r /workspace/hxclaw/.* /workspace/hxclaw/ 2>/dev/null || true
|
||||||
|
|
||||||
- name: Download dependencies
|
- name: Download dependencies
|
||||||
run: cd /workspace/hxclaw && go mod download
|
run: go mod download
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
@@ -39,13 +37,14 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: Checksums
|
- name: Checksums
|
||||||
run: cd /workspace/hxclaw && sha256sum hxclaw-* > checksums.txt
|
run: sha256sum hxclaw-* > checksums.txt
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: "${{ secrets.release_token }}"
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
cd /workspace/hxclaw
|
# 使用阿里云 apk 镜像源
|
||||||
|
echo -e 'https://mirrors.aliyun.com/alpine/v3.20/main/\nhttps://mirrors.aliyun.com/alpine/v3.20/community/' > /etc/apk/repositories
|
||||||
apk add curl jq
|
apk add curl jq
|
||||||
|
|
||||||
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
||||||
|
|||||||
57
.github/workflows/release.yml
vendored
57
.github/workflows/release.yml
vendored
@@ -1,57 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: golang:1.26-alpine
|
|
||||||
env:
|
|
||||||
GOPROXY: "https://mirrors.aliyun.com/goproxy/,direct"
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
run: |
|
|
||||||
apk add git bash
|
|
||||||
|
|
||||||
- name: Download dependencies
|
|
||||||
run: go mod download
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
for p in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
|
|
||||||
os=${p%/*}
|
|
||||||
arch=${p#*/}
|
|
||||||
ext=""
|
|
||||||
[ "$os" = "windows" ] && ext=".exe"
|
|
||||||
GOOS=$os GOARCH=$arch go build -o "hxclaw-${os}-${arch}${ext}" ./cmd/hxclaw
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Checksums
|
|
||||||
run: sha256sum hxclaw-* > checksums.txt
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.release_token }}
|
|
||||||
run: |
|
|
||||||
apk add curl jq
|
|
||||||
|
|
||||||
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
|
||||||
|
|
||||||
RELEASE_BODY=$(git tag -l --format='%(contents)' "$TAG_NAME" 2>/dev/null || git log -1 --format="%s%n%n%b" "$TAG_NAME" 2>/dev/null || echo "Release $TAG_NAME")
|
|
||||||
|
|
||||||
RELEASE_RESPONSE=$(curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/hxclaw/releases" \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"tag_name\":\"${TAG_NAME}\",\"name\":\"${TAG_NAME}\",\"body\":\"${RELEASE_BODY}\"}")
|
|
||||||
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
|
||||||
|
|
||||||
for f in hxclaw-* checksums.txt; do
|
|
||||||
[ -f "$f" ] && curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/hxclaw/releases/${RELEASE_ID}/assets" \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-F "attachment=@$f"
|
|
||||||
done
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,8 +1,8 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.ini
|
*.ini
|
||||||
|
|
||||||
hxclaw
|
hxclaw*
|
||||||
hxclaw.exe
|
hxclaw.exe*
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.fleet/
|
.fleet/
|
||||||
|
|||||||
166
agents.md
166
agents.md
@@ -41,80 +41,36 @@
|
|||||||
|
|
||||||
## 当前任务
|
## 当前任务
|
||||||
|
|
||||||
### v0.3.0 目标
|
### v0.1.0 目标
|
||||||
|
|
||||||
实现聊天记忆体功能:
|
实现流式输出功能:
|
||||||
1. 集成 libSQL (TursoDB) 数据库
|
1. 创建 go.mod 配置依赖
|
||||||
2. 实现会话维度的短期记忆
|
2. 实现 main.go 入口
|
||||||
3. 支持向量检索(硅基流动 API)
|
3. 实现流式 Provider 调用
|
||||||
4. 命令行支持(/new, /memory, /sessions)
|
4. 实时打印 token
|
||||||
|
5. 处理非流式 Provider 回退
|
||||||
|
6. Markdown 终端渲染(glamour)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 实现进度
|
## 实现进度
|
||||||
|
|
||||||
### v0.2.0 已完成功能
|
### v0.1.0 已完成功能
|
||||||
|
|
||||||
1. **TTS 语音朗读**
|
1. **流式输出(新流程)**
|
||||||
- 集成 mimo-tts client(TCP 连接)
|
|
||||||
- 配置文件开关(tts.enabled)
|
|
||||||
- 命令行切换(/tts on/off/status)
|
|
||||||
- 临时 TTS 前缀(`T 消息`)
|
|
||||||
- 动态提示符显示状态(👀 🔊)
|
|
||||||
- 静默失败处理(网络异常时仅记录日志)
|
|
||||||
|
|
||||||
2. **流式输出(新流程)**
|
|
||||||
- 等待 AI 返回完整响应
|
- 等待 AI 返回完整响应
|
||||||
- Markdown 转译
|
- Markdown 转译
|
||||||
- 模拟流式输出(从配置读取速度)
|
- 模拟流式输出(从配置读取速度)
|
||||||
- 效果更好,无残留问题
|
- 效果更好,无残留问题
|
||||||
|
|
||||||
3. **Markdown 渲染**
|
2. **Markdown 渲染**
|
||||||
- 使用 glamour 库渲染 Markdown
|
- 使用 glamour 库渲染 Markdown
|
||||||
- 支持多种主题(dark, light, dracula, tokyo-night 等)
|
- 支持多种主题(dark, light, dracula, tokyo-night 等)
|
||||||
- 通过 project.config.yml 配置主题
|
- 通过 project.config.yml 配置主题
|
||||||
|
|
||||||
4. **项目配置**
|
3. **项目配置**
|
||||||
- 通过 project.config.yml 统一管理配置项
|
- 通过 project.config.yml 统一管理配置项
|
||||||
- 支持流式速度、渲染主题、Logo、TTS 等配置
|
- 支持流式速度、渲染主题、Logo 等配置
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### v0.3.0 进度
|
|
||||||
|
|
||||||
1. **数据库层**
|
|
||||||
- 集成 libSQL (TursoDB)
|
|
||||||
- 创建 sessions 和 chats 表
|
|
||||||
- 实现 CRUD 操作
|
|
||||||
- 数据库保存在 `~/.config/hxclaw/hxclaw.db`
|
|
||||||
|
|
||||||
2. **独立上下文系统**
|
|
||||||
- 创建 GetContextPrompt() 返回会话摘要
|
|
||||||
- 注入到 ProcessDirect() 调用前
|
|
||||||
- 不再依赖 picoclaw session
|
|
||||||
|
|
||||||
3. **向量服务**
|
|
||||||
- 封装硅基流动 Embedding API
|
|
||||||
- 实现向量生成和相似度检索
|
|
||||||
|
|
||||||
4. **会话管理**
|
|
||||||
- 自动创建 Session(首次输入时)
|
|
||||||
- 手动创建(/new 命令)
|
|
||||||
- 消息摘要生成
|
|
||||||
|
|
||||||
5. **UI 优化**
|
|
||||||
- 合并状态显示到单行
|
|
||||||
- 金色图标 + 灰色文字
|
|
||||||
- 暗绿色"会话已保存"/暗红色"保存异常"
|
|
||||||
|
|
||||||
6. **双记忆系统合并**
|
|
||||||
- 读取 picoclaw 的 MEMORY.md 作为长期记忆
|
|
||||||
- 合并到 hxclaw 的会话摘要上下文
|
|
||||||
- AI 同时看到长期记忆和会话摘要
|
|
||||||
|
|
||||||
5. **JSON 导出**
|
|
||||||
- 退出时自动导出
|
|
||||||
- 手动导出
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -122,10 +78,11 @@
|
|||||||
|
|
||||||
### project.config.yml
|
### project.config.yml
|
||||||
|
|
||||||
配置文件位于项目根目录:
|
项目配置文件位于项目根目录:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# hxclaw 项目配置文件
|
# hxclaw 项目配置文件
|
||||||
|
# 项目级配置,会覆盖默认配置但被用户配置覆盖
|
||||||
|
|
||||||
# 模拟流式输出配置
|
# 模拟流式输出配置
|
||||||
streaming:
|
streaming:
|
||||||
@@ -135,75 +92,19 @@ streaming:
|
|||||||
# Markdown 渲染配置
|
# Markdown 渲染配置
|
||||||
markdown:
|
markdown:
|
||||||
theme: dark # 渲染主题:dark, light, dracula, tokyo-night 等
|
theme: dark # 渲染主题:dark, light, dracula, tokyo-night 等
|
||||||
line_width: -1 # 自动换行宽度(-1=禁用,0=自动,>0=固定宽度)
|
line_width: 0 # 自动换行宽度(0=自动,>0=固定宽度,-1=禁用)
|
||||||
|
|
||||||
# UI 配置
|
# UI 配置
|
||||||
ui:
|
ui:
|
||||||
logo: "🦐"
|
logo: "🦐" # Logo
|
||||||
user_icon: "👀 "
|
user_icon: "👀 " # 用户输入提示符
|
||||||
|
|
||||||
# TTS 语音配置
|
|
||||||
tts:
|
|
||||||
enabled: false # 全局开关(默认关闭)
|
|
||||||
port: 9876 # mimo-tts daemon 端口
|
|
||||||
auto: true # AI 回复后自动朗读
|
|
||||||
```
|
```
|
||||||
|
|
||||||
配置加载优先级:
|
配置加载优先级(从高到低):
|
||||||
1. 环境变量 `HXCLAW_CONFIG` 指定路径
|
1. 用户配置 `~/.config/hxclaw/config.yml`
|
||||||
2. 项目根目录 `project.config.yml`
|
2. 环境变量 `HXCLAW_CONFIG` 指定路径
|
||||||
|
3. 项目根目录 `project.config.yml`
|
||||||
---
|
4. 代码中的默认值
|
||||||
|
|
||||||
### 用户配置
|
|
||||||
|
|
||||||
用户配置文件位于 `~/.config/hxclaw/config.yml`,启动时自动创建。
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# hxclaw 用户配置文件
|
|
||||||
# 此文件位于 ~/.config/hxclaw/config.yml
|
|
||||||
# 用户配置优先于项目配置
|
|
||||||
|
|
||||||
# Markdown 渲染配置
|
|
||||||
markdown:
|
|
||||||
theme: dark # 渲染主题
|
|
||||||
line_width: 0 # 换行宽度
|
|
||||||
|
|
||||||
# UI 配置
|
|
||||||
ui:
|
|
||||||
logo: "🦐"
|
|
||||||
user_icon: "👀 "
|
|
||||||
|
|
||||||
# TTS 语音配置
|
|
||||||
tts:
|
|
||||||
enabled: false # 全局开关
|
|
||||||
auto: true # AI 回复后自动朗读
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TTS 使用指南
|
|
||||||
|
|
||||||
### 命令
|
|
||||||
|
|
||||||
| 输入 | 行为 |
|
|
||||||
|------|------|
|
|
||||||
| `/tts` | 切换 TTS 开关 |
|
|
||||||
| `/tts on` | 开启 TTS |
|
|
||||||
| `/tts off` | 关闭 TTS |
|
|
||||||
| `/tts status` | 显示 TTS 状态 |
|
|
||||||
| `T 消息` | 临时开启 TTS 并发送消息 |
|
|
||||||
|
|
||||||
### 动态提示符
|
|
||||||
|
|
||||||
- 关闭:`👀 `
|
|
||||||
- 开启:`👀 🔊 `
|
|
||||||
|
|
||||||
### 注意事项
|
|
||||||
|
|
||||||
- 需要先安装并启动 mimo-tts daemon:`mimo-tts daemon start`
|
|
||||||
- TTS 服务端地址:本地 9876 端口(默认)
|
|
||||||
- 网络异常时会静默失败,仅记录日志
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -216,30 +117,21 @@ tts:
|
|||||||
- `charm.land/x/term` - 终端控制
|
- `charm.land/x/term` - 终端控制
|
||||||
- `github.com/muesli/termenv` - 终端环境工具
|
- `github.com/muesli/termenv` - 终端环境工具
|
||||||
- `gopkg.in/yaml.v3` - 配置文件解析
|
- `gopkg.in/yaml.v3` - 配置文件解析
|
||||||
- `github.com/ergochat/readline` - 终端输入
|
|
||||||
|
|
||||||
### 配置文件
|
### 配置文件
|
||||||
|
|
||||||
- `cmd/hxclaw/main.go` - 主入口逻辑
|
- `cmd/hxclaw/main.go` - 主入口逻辑
|
||||||
|
- `cmd/hxclaw/internal/config.go` - 配置加载(支持用户配置和项目配置合并)
|
||||||
- `cmd/hxclaw/internal/markdown.go` - Markdown 渲染器
|
- `cmd/hxclaw/internal/markdown.go` - Markdown 渲染器
|
||||||
- `cmd/hxclaw/internal/helpers.go` - 辅助函数(Readline)
|
- `cmd/hxclaw/internal/helpers.go` - 辅助函数(Readline、SimpleReader)
|
||||||
- `cmd/hxclaw/internal/config.go` - 项目配置加载
|
- `cmd/hxclaw/internal/spinner.go` - 加载动画组件
|
||||||
- `cmd/hxclaw/internal/tts.go` - TTS 客户端
|
- `cmd/hxclaw/internal/markdown_test.go` - Markdown 测试
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 已知问题
|
## 已解决
|
||||||
|
|
||||||
1. **重绘残留**:某些情况下有轻微文本重复(已通过新流程解决)
|
1. ~~**重绘残留**:某些情况下有轻微文本重复~~ - 已通过新流程解决
|
||||||
2. **终端兼容性**:termenv 在某些终端可能不完全工作
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 待优化
|
|
||||||
|
|
||||||
1. 打印和 TTS 朗读同时进行(而非先打印完再读)
|
|
||||||
2. 添加更多主题支持
|
|
||||||
3. 添加命令行参数支持主题选择
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
337
changelog.md
337
changelog.md
@@ -2,140 +2,26 @@
|
|||||||
|
|
||||||
## 版本记录
|
## 版本记录
|
||||||
|
|
||||||
### v0.3.0 (2026-04-27)
|
### v0.1.0 (当前)
|
||||||
|
|
||||||
- **双记忆系统合并**
|
- [x] 流式输出功能
|
||||||
- 读取 picoclaw 的 MEMORY.md 作为长期记忆
|
- [x] Markdown 渲染功能
|
||||||
- 合并到 hxclaw 的会话摘要上下文
|
- [x] 配置系统(支持用户配置和项目配置)
|
||||||
- AI 同时看到长期记忆和会话摘要
|
- [x] 代码中文注释
|
||||||
|
|
||||||
- **独立上下文系统**
|
|
||||||
- 创建 GetContextPrompt() 返回会话摘要
|
|
||||||
- 注入到 ProcessDirect() 调用前
|
|
||||||
- 不再依赖 picoclaw session 管理
|
|
||||||
- 修复 recall 结果污染 session summary 问题
|
|
||||||
|
|
||||||
- **数据库层完善**
|
|
||||||
- 集成 libSQL (TursoDB)
|
|
||||||
- 创建 sessions 和 chats 表
|
|
||||||
- 实现 CRUD 操作
|
|
||||||
- 数据库保存在 `~/.config/hxclaw/hxclaw.db`
|
|
||||||
- 向量存储使用 binary 编码(float32)
|
|
||||||
|
|
||||||
- **向量检索功能**
|
|
||||||
- 硅基流动 BGE-M3 API 集成
|
|
||||||
- 向量生成和存储
|
|
||||||
- Cosine Similarity 计算
|
|
||||||
- SearchSimilar() 函数实现
|
|
||||||
- 4 个查询场景(RecallHistory, RecallTopic, RecallSession, RecallWithinSession)
|
|
||||||
|
|
||||||
- **三重检测机制**
|
|
||||||
- 关键词匹配(之前、聊过、记得等)
|
|
||||||
- 向量相似度自动检测(auto_recall + 阈值)
|
|
||||||
- /recall 命令强制触发
|
|
||||||
- 配置项:keywords, auto_recall, similarity_threshold, max_results
|
|
||||||
|
|
||||||
- **MongoDB 风格导出**
|
|
||||||
- 固定路径:`~/.config/hxclaw/export-data.json`
|
|
||||||
- chats 嵌套在 sessions 下
|
|
||||||
- 增量导出,同 session 累加
|
|
||||||
- 版本控制(version 字段)
|
|
||||||
|
|
||||||
- **UI 优化**
|
|
||||||
- 合并状态显示到单行(耗时 · 状态 · 消息数)
|
|
||||||
- 颜色设计:金色图标 + 灰色文字
|
|
||||||
- 暗绿色"会话已保存" / 暗红色"会话保存异常"
|
|
||||||
|
|
||||||
- **配置项更新**
|
|
||||||
- memory.recall 配置
|
|
||||||
- memory.vector.max_search_results
|
|
||||||
- memory.auto_export(替换 export_on_exit)
|
|
||||||
- 默认 max_search_results = 10
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### v0.2.1
|
### v0.2.0 (计划)
|
||||||
|
|
||||||
- 修复 TTS JSON 请求格式,兼容 Windows daemon
|
- [ ] 代码块渲染
|
||||||
- 发送格式改为 `{"text": "内容"}`
|
- [ ] 表格渲染
|
||||||
|
- [ ] 列表渲染
|
||||||
|
|
||||||
---
|
### v0.3.0 (计划)
|
||||||
|
|
||||||
### v0.2.0
|
- [ ] 代码高亮
|
||||||
|
- [ ] 集成 glow 或类似库
|
||||||
- 新增 TTS 语音朗读功能
|
- [ ] 支持常见语言语法高亮
|
||||||
- 集成 mimo-tts client 功能,通过 TCP 连接本地 daemon
|
|
||||||
- 支持配置文件开关(tts.enabled)
|
|
||||||
- 支持命令行切换(/tts on/off/status)
|
|
||||||
- 支持临时 TTS 前缀(`T 消息` 临时开启)
|
|
||||||
- 动态提示符显示 TTS 状态(👀 🔊)
|
|
||||||
- 静默失败处理(网络异常时警告日志)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### v0.1.0
|
|
||||||
|
|
||||||
- 创建 hxclaw 项目
|
|
||||||
- 实现流式输出功能
|
|
||||||
- Markdown 渲染(glamour,自动代码高亮)
|
|
||||||
- 项目配置化(project.config.yml)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 待实现功能
|
|
||||||
|
|
||||||
### 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 (当前)
|
|
||||||
|
|
||||||
- [x] 双记忆系统合并(picoclaw MEMORY.md + hxclaw 会话摘要)
|
|
||||||
- [x] 数据库层集成(libSQL)
|
|
||||||
- [x] 独立上下文系统(不再依赖 picoclaw session)
|
|
||||||
- [x] 会话摘要注入
|
|
||||||
- [x] UI 优化(合并显示、颜色设计)
|
|
||||||
- [x] 向量检索(硅基流动 API)
|
|
||||||
- [x] 4 个查询场景(RecallHistory, RecallTopic...)
|
|
||||||
- [x] 三重检测机制
|
|
||||||
- [x] MongoDB 风格导出
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 待实现功能
|
|
||||||
|
|
||||||
### v0.4.0 (计划)
|
|
||||||
|
|
||||||
- [ ] 命令行参数支持(--theme, --tts 等)
|
|
||||||
- [ ] 多语言支持
|
|
||||||
- [ ] /new 命令开始新会话
|
|
||||||
- [ ] /memory list|show 命令
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目前进度
|
|
||||||
|
|
||||||
- [x] 创建项目目录结构
|
|
||||||
- [x] 编写讨论记录(taolun.md)
|
|
||||||
- [x] 编写更新日志(changelog.md)
|
|
||||||
- [x] 编写 AI 行为指南(agents.md)
|
|
||||||
- [x] 创建 go.mod
|
|
||||||
- [x] 实现 main.go 入口
|
|
||||||
- [x] 实现流式输出核心逻辑
|
|
||||||
- [x] 编译成功,生成 hxclaw 二进制
|
|
||||||
- [x] 添加 spinner 加载动画组件
|
|
||||||
- [x] 实现 Markdown 渲染(glamour)
|
|
||||||
- [x] 实现项目配置化(project.config.yml)
|
|
||||||
- [x] 实现 TTS 语音朗读功能
|
|
||||||
- [x] 集成 libSQL 数据库
|
|
||||||
- [x] 实现独立上下文系统
|
|
||||||
- [x] UI 状态合并显示
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -167,14 +53,7 @@
|
|||||||
|
|
||||||
**问题**:不是所有 Provider 都支持流式输出
|
**问题**:不是所有 Provider 都支持流式输出
|
||||||
|
|
||||||
**纠正**:需要使用类型断言判断 Provider 是否实现 `providers.StreamingProvider` 接口:
|
**纠正**:需要使用类型断言判断 Provider 是否实现 `providers.StreamingProvider` 接口
|
||||||
```go
|
|
||||||
if sp, ok := provider.(providers.StreamingProvider); ok {
|
|
||||||
// 使用 ChatStream
|
|
||||||
} else {
|
|
||||||
// 使用普通 Chat
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**知识点**:picoclaw 的 Provider 设计使用了接口分离原则,流式是可选能力
|
**知识点**:picoclaw 的 Provider 设计使用了接口分离原则,流式是可选能力
|
||||||
|
|
||||||
@@ -185,8 +64,8 @@ if sp, ok := provider.(providers.StreamingProvider); ok {
|
|||||||
**问题**:如何实现 Markdown 终端渲染
|
**问题**:如何实现 Markdown 终端渲染
|
||||||
|
|
||||||
**纠正**:使用 charmbracelet 家族:
|
**纠正**:使用 charmbracelet 家族:
|
||||||
|
- glamour:Markdown 渲染
|
||||||
- lipgloss:样式定义
|
- lipgloss:样式定义
|
||||||
- glow:代码高亮
|
|
||||||
|
|
||||||
**知识点**:charmbracelet 是 Go 终端UI 的事实标准,API 设计优雅
|
**知识点**:charmbracelet 是 Go 终端UI 的事实标准,API 设计优雅
|
||||||
|
|
||||||
@@ -204,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 回调接收累积文本导致重复输出
|
### onChunk 回调接收累积文本导致重复输出
|
||||||
|
|
||||||
**问题**:picoclaw 的 `StreamingProvider` 接口定义:
|
**问题**:picoclaw 的 `StreamingProvider` 接口定义 `onChunk func(accumulated string)`,注释说明每次回调时参数是累积的完整文本(如 "你好" → "你好!再次" → "你好!再次见到"),而不是增量。
|
||||||
```go
|
|
||||||
onChunk func(accumulated string)
|
|
||||||
```
|
|
||||||
|
|
||||||
注释说明:"onChunk receives the accumulated text so far (not individual deltas)"。每次回调时参数是累积的完整文本(如 "你好" → "你好!再次" → "你好!再次见到"),而不是增量。
|
**纠正**:使用 `printedLen` 跟踪已打印位置,只打印新增部分
|
||||||
|
|
||||||
**纠正**:使用 `printedLen` 跟踪已打印位置,只打印新增部分:
|
|
||||||
```go
|
|
||||||
var printedLen int
|
|
||||||
func(accumulated string) {
|
|
||||||
if len(accumulated) > printedLen {
|
|
||||||
fmt.Print(accumulated[printedLen:])
|
|
||||||
printedLen = len(accumulated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**知识点**:picoclaw 故意设计为累积文本,这样可以在任意时刻获取完整内容用于调试
|
**知识点**: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 组件的 model 更新必须使用返回值
|
||||||
|
|
||||||
**问题**:spinner 动画不动
|
**问题**:spinner 动画不动
|
||||||
@@ -339,21 +101,7 @@ os.Stdout.Sync()
|
|||||||
|
|
||||||
**纠正**:spinner model 是值类型,需要使用返回值更新:
|
**纠正**:spinner model 是值类型,需要使用返回值更新:
|
||||||
```go
|
```go
|
||||||
// 错误写法
|
s.spinner, _ = s.spinner.Update(msg) // 必须使用返回值更新
|
||||||
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,需要显式使用返回值。
|
**知识点**:bubbletea v2 的组件遵循 TEA 架构模式,Update 方法返回更新后的 model,需要显式使用返回值。
|
||||||
@@ -364,36 +112,37 @@ func (s *Spinner) tick() {
|
|||||||
|
|
||||||
**问题**:spinner 使用 `\r` 回到行首刷新,流式输出也在同一行打印,导致内容混在一起
|
**问题**:spinner 使用 `\r` 回到行首刷新,流式输出也在同一行打印,导致内容混在一起
|
||||||
|
|
||||||
**现象**:
|
**纠正**:在第一个 token 时停止 spinner,让 spinner 输出 "思考完成." 并换行,然后再开始流式打印
|
||||||
```
|
|
||||||
⠋ 回答中... 好
|
|
||||||
⠋ 回答中... 注于
|
|
||||||
```
|
|
||||||
|
|
||||||
**纠正**:在第一个 token 时停止 spinner,让 spinner 输出 "思考完成." 并换行,然后再开始流式打印:
|
|
||||||
```go
|
|
||||||
if firstToken && len(accumulated) > 0 {
|
|
||||||
spinner.Stop() // 停止 spinner,会打印 "思考完成."
|
|
||||||
firstToken = false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**知识点**: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` 函数实现配置合并逻辑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 代码注释规范
|
||||||
|
|
||||||
|
**问题**:代码缺少注释,后续维护困难
|
||||||
|
|
||||||
|
**纠正**:所有代码添加详细中文注释:
|
||||||
|
- 包级别注释
|
||||||
|
- 结构体注释
|
||||||
|
- 字段注释(使用行内注释 `//`)
|
||||||
|
- 函数注释
|
||||||
|
- 关键逻辑注释
|
||||||
|
|
||||||
|
**知识点**:详细注释是团队协作和后续维护的基础
|
||||||
@@ -18,10 +18,6 @@ type Config struct {
|
|||||||
Markdown MarkdownConfig `yaml:"markdown"`
|
Markdown MarkdownConfig `yaml:"markdown"`
|
||||||
// UI UI 显示配置
|
// UI UI 显示配置
|
||||||
UI UIConfig `yaml:"ui"`
|
UI UIConfig `yaml:"ui"`
|
||||||
// TTS TTS 语音配置
|
|
||||||
TTS TTSConfig `yaml:"tts"`
|
|
||||||
// Memory 聊天记忆体配置
|
|
||||||
Memory MemoryConfig `yaml:"memory"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamingConfig 流式输出配置,控制模拟打字效果的延迟时间
|
// StreamingConfig 流式输出配置,控制模拟打字效果的延迟时间
|
||||||
@@ -48,58 +44,6 @@ type UIConfig struct {
|
|||||||
UserIcon string `yaml:"user_icon"`
|
UserIcon string `yaml:"user_icon"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TTSConfig TTS 语音配置
|
|
||||||
type TTSConfig struct {
|
|
||||||
// Enabled 全局开关
|
|
||||||
Enabled bool `yaml:"enabled"`
|
|
||||||
// Port 端口
|
|
||||||
Port int `yaml:"port"`
|
|
||||||
// Auto AI 回复后自动朗读
|
|
||||||
Auto bool `yaml:"auto"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryConfig 聊天记忆体配置
|
|
||||||
type MemoryConfig struct {
|
|
||||||
// Enabled 启用开关
|
|
||||||
Enabled bool `yaml:"enabled"`
|
|
||||||
// DBPath 数据库路径
|
|
||||||
DBPath string `yaml:"db_path"`
|
|
||||||
// AutoSession 自动创建 Session
|
|
||||||
AutoSession bool `yaml:"auto_session"`
|
|
||||||
// AutoExport 退出时自动导出
|
|
||||||
AutoExport bool `yaml:"auto_export"`
|
|
||||||
// Vector 向量服务配置
|
|
||||||
Vector VectorConfig `yaml:"vector"`
|
|
||||||
// Recall 检索配置
|
|
||||||
Recall RecallConfig `yaml:"recall"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// VectorConfig 向量服务配置
|
|
||||||
type VectorConfig struct {
|
|
||||||
// APIKey 硅基流动 API Key
|
|
||||||
APIKey string `yaml:"api_key"`
|
|
||||||
// BaseURL API 地址
|
|
||||||
BaseURL string `yaml:"base_url"`
|
|
||||||
// Model 向量模型
|
|
||||||
Model string `yaml:"model"`
|
|
||||||
// Dimension <20><>量维度
|
|
||||||
Dimension int `yaml:"dimension"`
|
|
||||||
// MaxSearchResults 最大检索结果数
|
|
||||||
MaxSearchResults int `yaml:"max_search_results"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecallConfig 检索配置
|
|
||||||
type RecallConfig struct {
|
|
||||||
// Keywords 触发关键词列表
|
|
||||||
Keywords []string `yaml:"keywords"`
|
|
||||||
// AutoRecall 是否自动检测相似度
|
|
||||||
AutoRecall bool `yaml:"auto_recall"`
|
|
||||||
// SimilarityThreshold 相似度阈值
|
|
||||||
SimilarityThreshold float64 `yaml:"similarity_threshold"`
|
|
||||||
// MaxResults 最大检索结果数
|
|
||||||
MaxResults int `yaml:"max_results"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// defaultCfg 默认配置值,当配置文件不存在或字段为空时使用
|
// defaultCfg 默认配置值,当配置文件不存在或字段为空时使用
|
||||||
defaultCfg = Config{
|
defaultCfg = Config{
|
||||||
@@ -109,43 +53,22 @@ var (
|
|||||||
},
|
},
|
||||||
Markdown: MarkdownConfig{
|
Markdown: MarkdownConfig{
|
||||||
Theme: "dark",
|
Theme: "dark",
|
||||||
LineWidth: -1,
|
LineWidth: 0,
|
||||||
},
|
},
|
||||||
UI: UIConfig{
|
UI: UIConfig{
|
||||||
Logo: "🦐",
|
Logo: "🦐",
|
||||||
UserIcon: "👀 ",
|
UserIcon: "👀 ",
|
||||||
},
|
},
|
||||||
TTS: TTSConfig{
|
|
||||||
Enabled: false,
|
|
||||||
Port: 9876,
|
|
||||||
Auto: true,
|
|
||||||
},
|
|
||||||
Memory: MemoryConfig{
|
|
||||||
Enabled: true,
|
|
||||||
DBPath: "",
|
|
||||||
AutoSession: true,
|
|
||||||
AutoExport: true,
|
|
||||||
Vector: VectorConfig{
|
|
||||||
APIKey: "",
|
|
||||||
BaseURL: "https://api.siliconflow.cn/v1",
|
|
||||||
Model: "BAAI/bge-m3",
|
|
||||||
Dimension: 1024,
|
|
||||||
MaxSearchResults: 10,
|
|
||||||
},
|
|
||||||
Recall: RecallConfig{
|
|
||||||
Keywords: []string{"之前", "聊过", "记得", "找找", "曾经", "谈论过", "提过"},
|
|
||||||
AutoRecall: true,
|
|
||||||
SimilarityThreshold: 0.7,
|
|
||||||
MaxResults: 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
cfg *Config // 已合并的配置(用户配置 + 项目配置)
|
cfg *Config // 已合并的配置(用户配置 + 项目配置)
|
||||||
cfgLock sync.RWMutex // 配置读写锁
|
cfgLock sync.RWMutex // 配置读写锁
|
||||||
|
userCfg *Config // 用户配置文件解析结果
|
||||||
|
userLock sync.RWMutex // 用户配置读写锁
|
||||||
)
|
)
|
||||||
|
|
||||||
// 用户配置文件路径常量
|
// 用户配置文件路径常量
|
||||||
const (
|
const (
|
||||||
|
userConfigDir = ".config/hxclaw" // 用户配置目录(相对于用户家目录)
|
||||||
userConfigFile = "config.yml" // 用户配置文件名
|
userConfigFile = "config.yml" // 用户配置文件名
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,8 +78,10 @@ const (
|
|||||||
func LoadProjectConfig() error {
|
func LoadProjectConfig() error {
|
||||||
cfgLock.Lock()
|
cfgLock.Lock()
|
||||||
defer cfgLock.Unlock()
|
defer cfgLock.Unlock()
|
||||||
|
userLock.Lock()
|
||||||
|
defer userLock.Unlock()
|
||||||
|
|
||||||
userCfg := loadUserConfig()
|
userCfg = loadUserConfig()
|
||||||
projCfg := loadProjectConfig()
|
projCfg := loadProjectConfig()
|
||||||
|
|
||||||
merged := mergeConfig(userCfg, projCfg)
|
merged := mergeConfig(userCfg, projCfg)
|
||||||
@@ -178,7 +103,7 @@ func loadUserConfig() *Config {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
createUserConfig(userPath)
|
createUserConfig(userPath)
|
||||||
return nil
|
return &defaultCfg
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -192,6 +117,7 @@ func loadUserConfig() *Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createUserConfig 创建默认的用户配置文件
|
// createUserConfig 创建默认的用户配置文件
|
||||||
|
// userPath: 配置文件完整路径
|
||||||
func createUserConfig(userPath string) error {
|
func createUserConfig(userPath string) error {
|
||||||
dir := filepath.Dir(userPath)
|
dir := filepath.Dir(userPath)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
@@ -200,39 +126,16 @@ func createUserConfig(userPath string) error {
|
|||||||
|
|
||||||
defaultContent := `# hxclaw 用户配置文件
|
defaultContent := `# hxclaw 用户配置文件
|
||||||
# 此文件位于 ~/.config/hxclaw/config.yml
|
# 此文件位于 ~/.config/hxclaw/config.yml
|
||||||
# 用户配置优先于项目配置
|
|
||||||
|
|
||||||
# Markdown 渲染配置
|
# Markdown 渲染配置
|
||||||
markdown:
|
markdown:
|
||||||
theme: dark
|
theme: dark # 渲染主题:dark, light, dracula, tokyo-night 等
|
||||||
line_width: 0
|
line_width: 0 # 自动换行宽度(0=自动,>0=固定宽度,-1=禁用)
|
||||||
|
|
||||||
# UI 配置
|
# UI 配置
|
||||||
ui:
|
ui:
|
||||||
logo: "🦐"
|
logo: "🦐"
|
||||||
user_icon: "👀 "
|
user_icon: "👀 "
|
||||||
|
|
||||||
# TTS 语音配置
|
|
||||||
tts:
|
|
||||||
enabled: false
|
|
||||||
auto: true
|
|
||||||
|
|
||||||
# 聊天记忆体配置
|
|
||||||
memory:
|
|
||||||
enabled: true
|
|
||||||
auto_session: true
|
|
||||||
auto_export: true
|
|
||||||
vector:
|
|
||||||
api_key: ""
|
|
||||||
base_url: "https://api.siliconflow.cn/v1"
|
|
||||||
model: "BAAI/bge-m3"
|
|
||||||
dimension: 1024
|
|
||||||
max_search_results: 10
|
|
||||||
recall:
|
|
||||||
keywords: ["之前", "聊过", "记得", "找找", "曾经", "谈论过", "提过"]
|
|
||||||
auto_recall: true
|
|
||||||
similarity_threshold: 0.7
|
|
||||||
max_results: 5
|
|
||||||
`
|
`
|
||||||
return os.WriteFile(userPath, []byte(defaultContent), 0644)
|
return os.WriteFile(userPath, []byte(defaultContent), 0644)
|
||||||
}
|
}
|
||||||
@@ -240,7 +143,7 @@ memory:
|
|||||||
// loadProjectConfig 加载项目级配置文件
|
// loadProjectConfig 加载项目级配置文件
|
||||||
// 路径优先级:环境变量 HXCLAW_CONFIG 指定路径 > ./project.config.yml
|
// 路径优先级:环境变量 HXCLAW_CONFIG 指定路径 > ./project.config.yml
|
||||||
func loadProjectConfig() *Config {
|
func loadProjectConfig() *Config {
|
||||||
cfgPath := GetProjectConfigPath()
|
cfgPath := getProjectConfigPath()
|
||||||
if cfgPath == "" {
|
if cfgPath == "" {
|
||||||
return &defaultCfg
|
return &defaultCfg
|
||||||
}
|
}
|
||||||
@@ -263,10 +166,10 @@ func loadProjectConfig() *Config {
|
|||||||
|
|
||||||
// mergeConfig 合并用户配置和项目配置
|
// mergeConfig 合并用户配置和项目配置
|
||||||
// 合并规则:用户配置优先于项目配置,项目配置优先于默认配置
|
// 合并规则:用户配置优先于项目配置,项目配置优先于默认配置
|
||||||
|
// 仅当配置值非空/非零时才覆盖默认值
|
||||||
func mergeConfig(userCfg, projCfg *Config) *Config {
|
func mergeConfig(userCfg, projCfg *Config) *Config {
|
||||||
result := defaultCfg
|
result := defaultCfg
|
||||||
|
|
||||||
// 先应用项目配置
|
|
||||||
if projCfg != nil {
|
if projCfg != nil {
|
||||||
if projCfg.Streaming.LineDelayMs > 0 {
|
if projCfg.Streaming.LineDelayMs > 0 {
|
||||||
result.Streaming.LineDelayMs = projCfg.Streaming.LineDelayMs
|
result.Streaming.LineDelayMs = projCfg.Streaming.LineDelayMs
|
||||||
@@ -286,120 +189,21 @@ func mergeConfig(userCfg, projCfg *Config) *Config {
|
|||||||
if projCfg.UI.UserIcon != "" {
|
if projCfg.UI.UserIcon != "" {
|
||||||
result.UI.UserIcon = projCfg.UI.UserIcon
|
result.UI.UserIcon = projCfg.UI.UserIcon
|
||||||
}
|
}
|
||||||
if projCfg.TTS.Port > 0 {
|
|
||||||
result.TTS.Port = projCfg.TTS.Port
|
|
||||||
}
|
|
||||||
// Memory 配置
|
|
||||||
if projCfg.Memory.Enabled {
|
|
||||||
result.Memory.Enabled = projCfg.Memory.Enabled
|
|
||||||
}
|
|
||||||
if projCfg.Memory.DBPath != "" {
|
|
||||||
result.Memory.DBPath = projCfg.Memory.DBPath
|
|
||||||
}
|
|
||||||
if projCfg.Memory.AutoSession {
|
|
||||||
result.Memory.AutoSession = projCfg.Memory.AutoSession
|
|
||||||
}
|
|
||||||
if projCfg.Memory.AutoExport {
|
|
||||||
result.Memory.AutoExport = projCfg.Memory.AutoExport
|
|
||||||
}
|
|
||||||
if projCfg.Memory.Vector.APIKey != "" {
|
|
||||||
result.Memory.Vector.APIKey = projCfg.Memory.Vector.APIKey
|
|
||||||
}
|
|
||||||
if projCfg.Memory.Vector.BaseURL != "" {
|
|
||||||
result.Memory.Vector.BaseURL = projCfg.Memory.Vector.BaseURL
|
|
||||||
}
|
|
||||||
if projCfg.Memory.Vector.Model != "" {
|
|
||||||
result.Memory.Vector.Model = projCfg.Memory.Vector.Model
|
|
||||||
}
|
|
||||||
if projCfg.Memory.Vector.Dimension > 0 {
|
|
||||||
result.Memory.Vector.Dimension = projCfg.Memory.Vector.Dimension
|
|
||||||
}
|
|
||||||
if projCfg.Memory.Vector.MaxSearchResults > 0 {
|
|
||||||
result.Memory.Vector.MaxSearchResults = projCfg.Memory.Vector.MaxSearchResults
|
|
||||||
}
|
|
||||||
// Recall 配置
|
|
||||||
if len(projCfg.Memory.Recall.Keywords) > 0 {
|
|
||||||
result.Memory.Recall.Keywords = projCfg.Memory.Recall.Keywords
|
|
||||||
}
|
|
||||||
if projCfg.Memory.Recall.AutoRecall {
|
|
||||||
result.Memory.Recall.AutoRecall = projCfg.Memory.Recall.AutoRecall
|
|
||||||
}
|
|
||||||
if projCfg.Memory.Recall.SimilarityThreshold > 0 {
|
|
||||||
result.Memory.Recall.SimilarityThreshold = projCfg.Memory.Recall.SimilarityThreshold
|
|
||||||
}
|
|
||||||
if projCfg.Memory.Recall.MaxResults > 0 {
|
|
||||||
result.Memory.Recall.MaxResults = projCfg.Memory.Recall.MaxResults
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 再应用用户配置(覆盖项目配置)
|
|
||||||
if userCfg != nil {
|
if userCfg != nil {
|
||||||
// Markdown
|
|
||||||
if userCfg.Markdown.Theme != "" {
|
if userCfg.Markdown.Theme != "" {
|
||||||
result.Markdown.Theme = userCfg.Markdown.Theme
|
result.Markdown.Theme = userCfg.Markdown.Theme
|
||||||
}
|
}
|
||||||
if userCfg.Markdown.LineWidth != 0 {
|
if userCfg.Markdown.LineWidth != 0 {
|
||||||
result.Markdown.LineWidth = userCfg.Markdown.LineWidth
|
result.Markdown.LineWidth = userCfg.Markdown.LineWidth
|
||||||
}
|
}
|
||||||
// UI
|
|
||||||
if userCfg.UI.Logo != "" {
|
if userCfg.UI.Logo != "" {
|
||||||
result.UI.Logo = userCfg.UI.Logo
|
result.UI.Logo = userCfg.UI.Logo
|
||||||
}
|
}
|
||||||
if userCfg.UI.UserIcon != "" {
|
if userCfg.UI.UserIcon != "" {
|
||||||
result.UI.UserIcon = userCfg.UI.UserIcon
|
result.UI.UserIcon = userCfg.UI.UserIcon
|
||||||
}
|
}
|
||||||
// TTS(用户配置可以覆盖 enabled 和 auto)
|
|
||||||
if userCfg.TTS.Enabled {
|
|
||||||
result.TTS.Enabled = userCfg.TTS.Enabled
|
|
||||||
}
|
|
||||||
if userCfg.TTS.Port > 0 {
|
|
||||||
result.TTS.Port = userCfg.TTS.Port
|
|
||||||
}
|
|
||||||
if userCfg.TTS.Auto {
|
|
||||||
result.TTS.Auto = userCfg.TTS.Auto
|
|
||||||
}
|
|
||||||
// Memory 配置(用户配置优先)
|
|
||||||
if userCfg.Memory.Enabled {
|
|
||||||
result.Memory.Enabled = userCfg.Memory.Enabled
|
|
||||||
}
|
|
||||||
if userCfg.Memory.DBPath != "" {
|
|
||||||
result.Memory.DBPath = userCfg.Memory.DBPath
|
|
||||||
}
|
|
||||||
if userCfg.Memory.AutoSession {
|
|
||||||
result.Memory.AutoSession = userCfg.Memory.AutoSession
|
|
||||||
}
|
|
||||||
if userCfg.Memory.AutoExport {
|
|
||||||
result.Memory.AutoExport = userCfg.Memory.AutoExport
|
|
||||||
}
|
|
||||||
// 向量 API Key 只能在用户配置中指定
|
|
||||||
if userCfg.Memory.Vector.APIKey != "" {
|
|
||||||
result.Memory.Vector.APIKey = userCfg.Memory.Vector.APIKey
|
|
||||||
}
|
|
||||||
if userCfg.Memory.Vector.BaseURL != "" {
|
|
||||||
result.Memory.Vector.BaseURL = userCfg.Memory.Vector.BaseURL
|
|
||||||
}
|
|
||||||
if userCfg.Memory.Vector.Model != "" {
|
|
||||||
result.Memory.Vector.Model = userCfg.Memory.Vector.Model
|
|
||||||
}
|
|
||||||
if userCfg.Memory.Vector.Dimension > 0 {
|
|
||||||
result.Memory.Vector.Dimension = userCfg.Memory.Vector.Dimension
|
|
||||||
}
|
|
||||||
if userCfg.Memory.Vector.MaxSearchResults > 0 {
|
|
||||||
result.Memory.Vector.MaxSearchResults = userCfg.Memory.Vector.MaxSearchResults
|
|
||||||
}
|
|
||||||
// Recall 配置
|
|
||||||
if len(userCfg.Memory.Recall.Keywords) > 0 {
|
|
||||||
result.Memory.Recall.Keywords = userCfg.Memory.Recall.Keywords
|
|
||||||
}
|
|
||||||
if userCfg.Memory.Recall.AutoRecall {
|
|
||||||
result.Memory.Recall.AutoRecall = userCfg.Memory.Recall.AutoRecall
|
|
||||||
}
|
|
||||||
if userCfg.Memory.Recall.SimilarityThreshold > 0 {
|
|
||||||
result.Memory.Recall.SimilarityThreshold = userCfg.Memory.Recall.SimilarityThreshold
|
|
||||||
}
|
|
||||||
if userCfg.Memory.Recall.MaxResults > 0 {
|
|
||||||
result.Memory.Recall.MaxResults = userCfg.Memory.Recall.MaxResults
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result
|
return &result
|
||||||
@@ -418,16 +222,21 @@ func GetProjectConfig() *Config {
|
|||||||
|
|
||||||
// getUserConfigPath 获取用户配置文件路径
|
// getUserConfigPath 获取用户配置文件路径
|
||||||
// 路径格式:~/.config/hxclaw/config.yml
|
// 路径格式:~/.config/hxclaw/config.yml
|
||||||
|
// 支持 Windows (USERPROFILE) 和 Unix (HOME) 环境变量
|
||||||
func getUserConfigPath() string {
|
func getUserConfigPath() string {
|
||||||
return GetConfigFile()
|
homeDir := os.Getenv("USERPROFILE")
|
||||||
|
if homeDir == "" {
|
||||||
|
homeDir = os.Getenv("HOME")
|
||||||
|
}
|
||||||
|
if homeDir == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, userConfigDir, userConfigFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserConfigDir 获取用户配置目录
|
// getProjectConfigPath 获取项目配置文件路径
|
||||||
func GetUserConfigDir() string {
|
// 优先使用环境变量 HXCLAW_CONFIG 指定的路径,否则使用当前目录的 project.config.yml
|
||||||
return GetConfigDir()
|
func getProjectConfigPath() string {
|
||||||
}
|
|
||||||
|
|
||||||
func GetProjectConfigPath() string {
|
|
||||||
if path := os.Getenv("HXCLAW_CONFIG"); path != "" {
|
if path := os.Getenv("HXCLAW_CONFIG"); path != "" {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ func GetConfigPath() string {
|
|||||||
// Readline 实例包装
|
// Readline 实例包装
|
||||||
type Readline struct {
|
type Readline struct {
|
||||||
rl *readline.Instance
|
rl *readline.Instance
|
||||||
basePrompt string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReadline 创建一个新的 Readline 实例
|
// NewReadline 创建一个新的 Readline 实例
|
||||||
@@ -69,18 +68,7 @@ func NewReadline(prompt string) (*Readline, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Readline{rl: rl, basePrompt: prompt}, nil
|
return &Readline{rl: rl}, nil
|
||||||
}
|
|
||||||
|
|
||||||
// SetPrompt 更新提示符
|
|
||||||
func (r *Readline) SetPrompt(prompt string) {
|
|
||||||
r.basePrompt = prompt
|
|
||||||
r.rl.SetPrompt(prompt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBasePrompt 返回基础提示符
|
|
||||||
func (r *Readline) GetBasePrompt() string {
|
|
||||||
return r.basePrompt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Readline 读取一行输入
|
// Readline 读取一行输入
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package internal 包含 hxclaw 的内部工具模块
|
||||||
|
// 提供配置管理、Markdown 渲染、输入读取等功能
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -9,6 +11,8 @@ import (
|
|||||||
"github.com/charmbracelet/x/term"
|
"github.com/charmbracelet/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RenderMarkdown 将 Markdown 文本渲染为终端友好的格式
|
||||||
|
// 支持通过配置或环境变量指定渲染主题和换行宽度
|
||||||
func RenderMarkdown(md string) string {
|
func RenderMarkdown(md string) string {
|
||||||
if md == "" {
|
if md == "" {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package internal 包含 hxclaw 的内部工具模块
|
||||||
|
// 提供配置管理、Markdown 渲染、输入读取等功能
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -9,22 +11,26 @@ import (
|
|||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SpinnerState 表示加载动画的状态
|
||||||
type SpinnerState int
|
type SpinnerState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StateThinking SpinnerState = iota
|
StateThinking SpinnerState = iota // 思考中状态
|
||||||
StateAnswering
|
StateAnswering // 回答中状态
|
||||||
StateDone
|
StateDone // 完成状态
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Spinner 加载动画组件,用于显示思考状态
|
||||||
type Spinner struct {
|
type Spinner struct {
|
||||||
text string
|
text string // 显示的文本内容
|
||||||
state SpinnerState
|
state SpinnerState // 当前状态
|
||||||
spinner spinner.Model
|
spinner spinner.Model // 底层动画模型
|
||||||
stopCh chan struct{}
|
stopCh chan struct{} // 停止信号通道
|
||||||
doneCh chan struct{}
|
doneCh chan struct{} // 完成信号通道
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSpinner 创建一个新的加载动画实例
|
||||||
|
// text: 初始显示的文本内容
|
||||||
func NewSpinner(text string) *Spinner {
|
func NewSpinner(text string) *Spinner {
|
||||||
s := spinner.New(
|
s := spinner.New(
|
||||||
spinner.WithSpinner(spinner.MiniDot),
|
spinner.WithSpinner(spinner.MiniDot),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package main 是 hxclaw 应用程序的入口包
|
||||||
|
// 提供交互式 CLI 界面和流式输出功能
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,7 +13,6 @@ import (
|
|||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
|
||||||
"github.com/hxclaw/hxclaw/cmd/hxclaw/internal"
|
"github.com/hxclaw/hxclaw/cmd/hxclaw/internal"
|
||||||
"github.com/hxclaw/hxclaw/cmd/hxclaw/internal/memory"
|
|
||||||
"github.com/muesli/termenv"
|
"github.com/muesli/termenv"
|
||||||
"github.com/sipeed/picoclaw/pkg/agent"
|
"github.com/sipeed/picoclaw/pkg/agent"
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
@@ -19,43 +20,53 @@ import (
|
|||||||
"github.com/sipeed/picoclaw/pkg/providers"
|
"github.com/sipeed/picoclaw/pkg/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Logo 应用 Logo 字符常量
|
||||||
const Logo = "🦐"
|
const Logo = "🦐"
|
||||||
|
|
||||||
var currentSession *memory.Session
|
// main 程序入口函数
|
||||||
|
// 负责初始化配置、创建 Provider、启动 Agent Loop 和交互式界面
|
||||||
func main() {
|
func main() {
|
||||||
|
// 加载 hxclaw 项目配置
|
||||||
if err := internal.LoadProjectConfig(); err != nil {
|
if err := internal.LoadProjectConfig(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "错误:加载项目配置失败: %v\n", err)
|
fmt.Fprintf(os.Stderr, "错误:加载项目配置失败: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打印应用 Logo 和欢迎信息
|
||||||
logo := internal.GetProjectConfig().UI.Logo
|
logo := internal.GetProjectConfig().UI.Logo
|
||||||
fmt.Printf("%s HxClaw - PicoClaw 增强版 CLI\n\n", logo)
|
fmt.Printf("%s HxClaw - PicoClaw 增强版 CLI\n\n", logo)
|
||||||
|
|
||||||
|
// 加载 picoclaw 配置文件
|
||||||
cfg, err := internal.LoadConfig()
|
cfg, err := internal.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "错误:加载配置失败: %v\n", err)
|
fmt.Fprintf(os.Stderr, "错误:加载配置失败: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配置日志系统
|
||||||
logger.ConfigureFromEnv()
|
logger.ConfigureFromEnv()
|
||||||
|
|
||||||
|
// 创建 AI Provider(支持 OpenAI、Claude 等)
|
||||||
provider, modelID, err := providers.CreateProvider(cfg)
|
provider, modelID, err := providers.CreateProvider(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "错误:创建 Provider 失败: %v\n", err)
|
fmt.Fprintf(os.Stderr, "错误:创建 Provider 失败: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果命令行指定了模型 ID,覆盖配置文件中的默认模型
|
||||||
if modelID != "" {
|
if modelID != "" {
|
||||||
cfg.Agents.Defaults.ModelName = modelID
|
cfg.Agents.Defaults.ModelName = modelID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建消息总线,用于组件间通信
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
defer msgBus.Close()
|
defer msgBus.Close()
|
||||||
|
|
||||||
|
// 创建 Agent Loop,处理用户交互和 AI 请求
|
||||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
||||||
defer agentLoop.Close()
|
defer agentLoop.Close()
|
||||||
|
|
||||||
|
// 获取启动信息并打印
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupInfo := agentLoop.GetStartupInfo()
|
||||||
logger.InfoCF("hxclaw", "HxClaw 已初始化",
|
logger.InfoCF("hxclaw", "HxClaw 已初始化",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
@@ -64,33 +75,15 @@ func main() {
|
|||||||
"skills_available": startupInfo["skills"].(map[string]any)["available"],
|
"skills_available": startupInfo["skills"].(map[string]any)["available"],
|
||||||
})
|
})
|
||||||
|
|
||||||
memoryCfg := internal.GetProjectConfig().Memory
|
// 打印交互模式提示
|
||||||
if memoryCfg.Enabled {
|
|
||||||
// 优先使用用户配置中的 db_path,如果没有则使用默认路径
|
|
||||||
dbPath := memoryCfg.DBPath
|
|
||||||
if dbPath == "" {
|
|
||||||
dbPath = memory.GetDefaultDBPath()
|
|
||||||
}
|
|
||||||
fmt.Printf("初始化记忆体,db_path: %s\n", dbPath)
|
|
||||||
if err := memory.Init(memory.WithDBPath(dbPath)); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "警告:初始化记忆体失败: %v,将使用无记忆模式\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println("记忆体初始化成功")
|
|
||||||
memory.InitVector(
|
|
||||||
memory.WithAPIKey(memoryCfg.Vector.APIKey),
|
|
||||||
memory.WithBaseURL(memoryCfg.Vector.BaseURL),
|
|
||||||
memory.WithModel(memoryCfg.Vector.Model),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", Logo)
|
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", Logo)
|
||||||
|
|
||||||
|
// 启动交互模式
|
||||||
interactiveMode(agentLoop, "cli:default")
|
interactiveMode(agentLoop, "cli:default")
|
||||||
}
|
}
|
||||||
|
|
||||||
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
basePrompt := internal.GetProjectConfig().UI.UserIcon
|
prompt := internal.GetProjectConfig().UI.UserIcon
|
||||||
prompt := internal.GetTTSPrompt(basePrompt)
|
|
||||||
|
|
||||||
rl, err := internal.NewReadline(prompt)
|
rl, err := internal.NewReadline(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,17 +94,11 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|||||||
}
|
}
|
||||||
defer rl.Close()
|
defer rl.Close()
|
||||||
|
|
||||||
ttsCfg := internal.GetProjectConfig().TTS
|
|
||||||
if ttsCfg.Enabled {
|
|
||||||
internal.SetTTSEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
line, err := rl.Readline()
|
line, err := rl.Readline()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == internal.ErrInterrupt || err == internal.ErrEOF {
|
if err == internal.ErrInterrupt || err == internal.ErrEOF {
|
||||||
fmt.Println("\n再见!")
|
fmt.Println("\n再见!")
|
||||||
memory.ExportIfNeeded()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("读取输入错误: %v\n", err)
|
fmt.Printf("读取输入错误: %v\n", err)
|
||||||
@@ -125,61 +112,21 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|||||||
|
|
||||||
if input == "exit" || input == "quit" {
|
if input == "exit" || input == "quit" {
|
||||||
fmt.Println("再见!")
|
fmt.Println("再见!")
|
||||||
memory.ExportIfNeeded()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isTempTTS := false
|
runWithStreaming(agentLoop, input, sessionKey)
|
||||||
if len(input) > 0 && input[0] == 'T' && (len(input) == 1 || input[1] == ' ') {
|
|
||||||
input = strings.TrimPrefix(input, "T")
|
|
||||||
input = strings.TrimPrefix(input, " ")
|
|
||||||
isTempTTS = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(input, "/tts") {
|
|
||||||
handleTTSCommand(input, rl, basePrompt)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(input, "/new") {
|
|
||||||
handleNewSessionCommand(rl, basePrompt)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(input, "/memory") {
|
|
||||||
handleMemoryCommand(input)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(input, "/sessions") {
|
|
||||||
handleSessionsCommand()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if isTempTTS {
|
|
||||||
enabled := internal.ToggleTTS()
|
|
||||||
if enabled {
|
|
||||||
rl.SetPrompt(internal.GetTTSPrompt(basePrompt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runWithStreaming(agentLoop, input, sessionKey, isTempTTS)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
reader := internal.NewSimpleReader()
|
reader := internal.NewSimpleReader()
|
||||||
ttsCfg := internal.GetProjectConfig().TTS
|
|
||||||
if ttsCfg.Enabled {
|
|
||||||
internal.SetTTSEnabled(true)
|
|
||||||
}
|
|
||||||
for {
|
for {
|
||||||
fmt.Print(internal.GetTTSPrompt(internal.GetProjectConfig().UI.UserIcon))
|
fmt.Print(internal.GetProjectConfig().UI.UserIcon)
|
||||||
line, err := reader.ReadString()
|
line, err := reader.ReadString()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == internal.ErrEOF {
|
if err == internal.ErrEOF {
|
||||||
fmt.Println("\n再见!")
|
fmt.Println("\n再见!")
|
||||||
memory.ExportIfNeeded()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("读取输入错误: %v\n", err)
|
fmt.Printf("读取输入错误: %v\n", err)
|
||||||
@@ -193,98 +140,49 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|||||||
|
|
||||||
if input == "exit" || input == "quit" {
|
if input == "exit" || input == "quit" {
|
||||||
fmt.Println("再见!")
|
fmt.Println("再见!")
|
||||||
memory.ExportIfNeeded()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isTempTTS := false
|
runWithStreaming(agentLoop, input, sessionKey)
|
||||||
if len(input) > 0 && input[0] == 'T' && (len(input) == 1 || input[1] == ' ') {
|
|
||||||
input = strings.TrimPrefix(input, "T")
|
|
||||||
input = strings.TrimPrefix(input, " ")
|
|
||||||
isTempTTS = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(input, "/tts") {
|
|
||||||
handleTTSCommandSimple(input)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(input, "/new") {
|
|
||||||
handleNewSessionCommand(nil, internal.GetProjectConfig().UI.UserIcon)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(input, "/memory") {
|
|
||||||
handleMemoryCommand(input)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(input, "/sessions") {
|
|
||||||
handleSessionsCommand()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if isTempTTS {
|
|
||||||
internal.ToggleTTS()
|
|
||||||
}
|
|
||||||
|
|
||||||
runWithStreaming(agentLoop, input, sessionKey, isTempTTS)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runWithStreaming 使用 ProcessDirect 处理请求,支持工具调用和结果显示
|
// runWithStreaming 使用 ProcessDirect 处理请求并以模拟流式方式输出结果
|
||||||
func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string, tempTTS bool) {
|
// 1. 显示加载动画表示 AI 正在思考
|
||||||
|
// 2. 调用 Agent 处理用户输入
|
||||||
|
// 3. 渲染 Markdown 输出并按行延迟显示
|
||||||
|
// 4. 打印处理耗时
|
||||||
|
func runWithStreaming(agentLoop *agent.AgentLoop, input, sessionKey string) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
// 保存原始输入用于后续保存
|
// 创建并启动加载动画
|
||||||
originalInput := input
|
|
||||||
|
|
||||||
// 注入 hxclaw 的上下文摘要
|
|
||||||
memoryCfg := internal.GetProjectConfig().Memory
|
|
||||||
if memoryCfg.Enabled {
|
|
||||||
contextPrompt := memory.GetContextPrompt(input)
|
|
||||||
if contextPrompt != "" {
|
|
||||||
input = contextPrompt + "\n用户新问题: " + input
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spinner := internal.NewSpinner("思考中...")
|
spinner := internal.NewSpinner("思考中...")
|
||||||
spinner.Start()
|
spinner.Start()
|
||||||
|
|
||||||
|
// 调用 AI 处理用户输入
|
||||||
resp, err := agentLoop.ProcessDirect(context.Background(), input, sessionKey)
|
resp, err := agentLoop.ProcessDirect(context.Background(), input, sessionKey)
|
||||||
|
|
||||||
|
// 停止加载动画
|
||||||
spinner.Stop()
|
spinner.Stop()
|
||||||
|
|
||||||
|
// 处理错误
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("警告: %v\n", err)
|
fmt.Printf("错误: %v\n", err)
|
||||||
}
|
|
||||||
|
|
||||||
if resp == "" {
|
|
||||||
fmt.Println("(空响应,跳过保存)")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染 Markdown 并输出
|
||||||
rendered := internal.RenderMarkdown(resp)
|
rendered := internal.RenderMarkdown(resp)
|
||||||
clearSpinnerLine()
|
clearSpinnerLine()
|
||||||
outputLineByLine(rendered)
|
outputLineByLine(rendered)
|
||||||
|
|
||||||
ttsCfg := internal.GetProjectConfig().TTS
|
// 打印处理耗时
|
||||||
if ttsCfg.Enabled || tempTTS || internal.IsTTSEnabled() {
|
|
||||||
go internal.SpeakText(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存聊天记录到数据库
|
|
||||||
var chatCount int
|
|
||||||
var saveErr error
|
|
||||||
memoryCfg = internal.GetProjectConfig().Memory
|
|
||||||
if memoryCfg.Enabled {
|
|
||||||
chatCount, saveErr = memory.SaveChat(originalInput, resp, !memory.ShouldSkipSummaryUpdate(originalInput))
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsed := time.Since(startTime)
|
elapsed := time.Since(startTime)
|
||||||
printElapsed(elapsed, chatCount, saveErr)
|
printElapsed(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clearSpinnerLine 清除 spinner 行
|
||||||
|
// 使用终端控制字符清除当前行并移动到行首
|
||||||
func clearSpinnerLine() {
|
func clearSpinnerLine() {
|
||||||
output := termenv.DefaultOutput()
|
output := termenv.DefaultOutput()
|
||||||
output.ClearLine()
|
output.ClearLine()
|
||||||
@@ -292,6 +190,9 @@ func clearSpinnerLine() {
|
|||||||
os.Stdout.Sync()
|
os.Stdout.Sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outputLineByLine 按行输出文本,模拟打字效果
|
||||||
|
// 每行之间根据配置延迟,最后一行延迟时间较长
|
||||||
|
// 空行会直接输出不延迟
|
||||||
func outputLineByLine(text string) {
|
func outputLineByLine(text string) {
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return
|
return
|
||||||
@@ -300,10 +201,12 @@ func outputLineByLine(text string) {
|
|||||||
lines := strings.Split(text, "\n")
|
lines := strings.Split(text, "\n")
|
||||||
totalLines := len(lines)
|
totalLines := len(lines)
|
||||||
|
|
||||||
|
// 获取延迟配置
|
||||||
cfg := internal.GetProjectConfig()
|
cfg := internal.GetProjectConfig()
|
||||||
lineDelay := time.Duration(cfg.Streaming.LineDelayMs) * time.Millisecond
|
lineDelay := time.Duration(cfg.Streaming.LineDelayMs) * time.Millisecond
|
||||||
lastLineDelay := time.Duration(cfg.Streaming.LastLineDelayMs) * time.Millisecond
|
lastLineDelay := time.Duration(cfg.Streaming.LastLineDelayMs) * time.Millisecond
|
||||||
|
|
||||||
|
// 逐行输出
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
if line == "" {
|
if line == "" {
|
||||||
lipgloss.Print("\n")
|
lipgloss.Print("\n")
|
||||||
@@ -312,6 +215,7 @@ func outputLineByLine(text string) {
|
|||||||
|
|
||||||
lipgloss.Print(line + "\n")
|
lipgloss.Print(line + "\n")
|
||||||
|
|
||||||
|
// 非最后一行使用普通延迟,最后一行使用较长延迟
|
||||||
if i < totalLines-1 {
|
if i < totalLines-1 {
|
||||||
time.Sleep(lineDelay)
|
time.Sleep(lineDelay)
|
||||||
} else {
|
} else {
|
||||||
@@ -322,36 +226,25 @@ func outputLineByLine(text string) {
|
|||||||
lipgloss.Print("\n")
|
lipgloss.Print("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 输出样式定义
|
||||||
var (
|
var (
|
||||||
iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e"))
|
iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e")) // 图标样式(金色)
|
||||||
textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32"))
|
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) {
|
// printElapsed 打印处理耗时信息
|
||||||
|
// 格式化输出:小于 60 秒显示秒数,否则显示分钟数
|
||||||
|
func printElapsed(elapsed time.Duration) {
|
||||||
elapsedSec := math.Round(elapsed.Seconds()*10) / 10
|
elapsedSec := math.Round(elapsed.Seconds()*10) / 10
|
||||||
elapsedStr := formatDuration(elapsedSec)
|
elapsedStr := formatDuration(elapsedSec)
|
||||||
|
|
||||||
icon := iconStyle.Render("▣ ")
|
icon := iconStyle.Render("▣ ")
|
||||||
timeText := textStyle.Render(fmt.Sprintf("耗时: %s", elapsedStr))
|
text := textStyle.Render(fmt.Sprintf("耗时: %s", elapsedStr))
|
||||||
|
fmt.Printf(" %s%s\n\n", icon, text)
|
||||||
var statusText string
|
|
||||||
if saveErr != nil {
|
|
||||||
statusText = memoryErrStyle.Render("会话保存异常")
|
|
||||||
} else if chatCount > 0 {
|
|
||||||
statusText = memoryOkStyle.Render("会话已保存")
|
|
||||||
}
|
|
||||||
|
|
||||||
memCountText := textStyle.Render(fmt.Sprintf("当前会话 %d 条消息", chatCount))
|
|
||||||
|
|
||||||
if statusText != "" {
|
|
||||||
fmt.Printf(" %s%s · %s · %s\n\n", icon, timeText, statusText, memCountText)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" %s%s · %s\n\n", icon, timeText, memCountText)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatTokens 格式化 token 数量
|
||||||
|
// 1000 以上显示为 k 单位(如 1.5k)
|
||||||
func formatTokens(n int) string {
|
func formatTokens(n int) string {
|
||||||
if n >= 1000 {
|
if n >= 1000 {
|
||||||
return fmt.Sprintf("%.1fk", float64(n)/1000)
|
return fmt.Sprintf("%.1fk", float64(n)/1000)
|
||||||
@@ -359,157 +252,11 @@ func formatTokens(n int) string {
|
|||||||
return fmt.Sprintf("%d", n)
|
return fmt.Sprintf("%d", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatDuration 格式化时长字符串
|
||||||
|
// 小于 60 秒显示秒数(如 12.5s),否则显示分钟数(如 2.5m)
|
||||||
func formatDuration(s float64) string {
|
func formatDuration(s float64) string {
|
||||||
if s >= 60 {
|
if s >= 60 {
|
||||||
return fmt.Sprintf("%.1fm", s/60)
|
return fmt.Sprintf("%.1fm", s/60)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%.1fs", s)
|
return fmt.Sprintf("%.1fs", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTTSCommand(input string, rl *internal.Readline, basePrompt string) {
|
|
||||||
args := strings.Fields(input)
|
|
||||||
if len(args) == 1 {
|
|
||||||
enabled := internal.ToggleTTS()
|
|
||||||
rl.SetPrompt(internal.GetTTSPrompt(basePrompt))
|
|
||||||
status := "关闭"
|
|
||||||
if enabled {
|
|
||||||
status = "开启"
|
|
||||||
}
|
|
||||||
fmt.Printf("TTS 已%s\n", status)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args[1] {
|
|
||||||
case "on":
|
|
||||||
internal.SetTTSEnabled(true)
|
|
||||||
rl.SetPrompt(internal.GetTTSPrompt(basePrompt))
|
|
||||||
fmt.Println("TTS 已开启")
|
|
||||||
case "off":
|
|
||||||
internal.SetTTSEnabled(false)
|
|
||||||
rl.SetPrompt(internal.GetTTSPrompt(basePrompt))
|
|
||||||
fmt.Println("TTS 已关闭")
|
|
||||||
case "status":
|
|
||||||
status := "关闭"
|
|
||||||
if internal.IsTTSEnabled() {
|
|
||||||
status = "开启"
|
|
||||||
}
|
|
||||||
fmt.Printf("TTS 状态: %s\n", status)
|
|
||||||
default:
|
|
||||||
fmt.Println("用法: /tts [on|off|status]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleTTSCommandSimple(input string) {
|
|
||||||
args := strings.Fields(input)
|
|
||||||
if len(args) == 1 {
|
|
||||||
internal.ToggleTTS()
|
|
||||||
status := "关闭"
|
|
||||||
if internal.IsTTSEnabled() {
|
|
||||||
status = "开启"
|
|
||||||
}
|
|
||||||
fmt.Printf("TTS 已%s\n", status)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args[1] {
|
|
||||||
case "on":
|
|
||||||
internal.SetTTSEnabled(true)
|
|
||||||
fmt.Println("TTS 已开启")
|
|
||||||
case "off":
|
|
||||||
internal.SetTTSEnabled(false)
|
|
||||||
fmt.Println("TTS 已关闭")
|
|
||||||
case "status":
|
|
||||||
status := "关闭"
|
|
||||||
if internal.IsTTSEnabled() {
|
|
||||||
status = "开启"
|
|
||||||
}
|
|
||||||
fmt.Printf("TTS 状态: %s\n", status)
|
|
||||||
default:
|
|
||||||
fmt.Println("用法: /tts [on|off|status]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleNewSessionCommand(rl *internal.Readline, basePrompt string) {
|
|
||||||
uuid, err := memory.CreateNewSession()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("创建新会话失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("已创建新会话: %s\n", uuid)
|
|
||||||
currentSession = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleMemoryCommand(input string) {
|
|
||||||
args := strings.Fields(input)
|
|
||||||
if len(args) == 1 || args[1] == "list" {
|
|
||||||
sessions, err := memory.ListSessions()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("查询会话失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(sessions) == 0 {
|
|
||||||
fmt.Println("暂无会话记录")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("共有 %d 个会话记录:\n", len(sessions))
|
|
||||||
for _, s := range sessions {
|
|
||||||
summary := s.Summary
|
|
||||||
if summary == "" {
|
|
||||||
summary = "(无摘要)"
|
|
||||||
} else if len(summary) > 50 {
|
|
||||||
summary = summary[:50] + "..."
|
|
||||||
}
|
|
||||||
fmt.Printf(" - %s: %s\n", s.UUID[:8], summary)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args[1] {
|
|
||||||
case "show":
|
|
||||||
session := memory.GetCurrentSession()
|
|
||||||
if session == nil {
|
|
||||||
latest, err := memory.GetLatestSession()
|
|
||||||
if err != nil || latest == nil {
|
|
||||||
fmt.Println("暂无会话记录")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
session = latest
|
|
||||||
}
|
|
||||||
if session.Summary == "" {
|
|
||||||
fmt.Println("当前会话暂无摘要")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("=== 会话摘要 (%s) ===\n%s\n", session.UUID[:8], session.Summary)
|
|
||||||
case "current":
|
|
||||||
if currentSession == nil {
|
|
||||||
fmt.Println("当前无活跃会话")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("当前会话: %s\n", currentSession.UUID)
|
|
||||||
fmt.Printf("聊天记录数: %d\n", len(currentSession.ChatIDs))
|
|
||||||
default:
|
|
||||||
fmt.Println("用法: /memory [list|show|current]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSessionsCommand() {
|
|
||||||
sessions, err := memory.ListSessions()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("查询会话失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(sessions) == 0 {
|
|
||||||
fmt.Println("暂无会话记录")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("共有 %d 个会话记录:\n", len(sessions))
|
|
||||||
for _, s := range sessions {
|
|
||||||
summary := s.Summary
|
|
||||||
if summary == "" {
|
|
||||||
summary = "(无摘要)"
|
|
||||||
} else if len(summary) > 30 {
|
|
||||||
summary = summary[:30] + "..."
|
|
||||||
}
|
|
||||||
fmt.Printf(" %s | %d 条消息 | %s\n", s.UUID[:8], len(s.ChatIDs), summary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,461 +0,0 @@
|
|||||||
# hxclaw 记忆体系统架构图
|
|
||||||
|
|
||||||
## 一、数据流向总图
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph 用户输入
|
|
||||||
A[用户输入]
|
|
||||||
A1[普通对话]
|
|
||||||
A2[查询历史]
|
|
||||||
A3[/recall 命令]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 上下文注入
|
|
||||||
B[GetContextPrompt]
|
|
||||||
B0[读取 picoclaw MEMORY.md]
|
|
||||||
B1[获取当前 Session 摘要]
|
|
||||||
B2[检测查询意图]
|
|
||||||
B3[按需调用 Recall]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph AI 处理
|
|
||||||
C[ProcessDirect]
|
|
||||||
C1[工具调用]
|
|
||||||
C2[多轮对话]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 保存流程
|
|
||||||
D[SaveChat]
|
|
||||||
D1[INSERT chat]
|
|
||||||
D2[UPDATE chat 摘要+向量]
|
|
||||||
D3[UPDATE session 摘要+向量]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 数据库
|
|
||||||
E[sessions 表]
|
|
||||||
F[chats 表]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 向量服务
|
|
||||||
G[硅基流动 API]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 外部记忆
|
|
||||||
H[picoclaw MEMORY.md]
|
|
||||||
end
|
|
||||||
|
|
||||||
A --> B
|
|
||||||
B0 --> B
|
|
||||||
H --> B0
|
|
||||||
B --> C
|
|
||||||
C --> D
|
|
||||||
D --> F
|
|
||||||
D --> E
|
|
||||||
F --> G
|
|
||||||
E --> G
|
|
||||||
|
|
||||||
A1 -->|普通对话| B1
|
|
||||||
A2 -->|检测关键词| B2
|
|
||||||
A3 -->|强制触发| B3
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、对话流程(默认模式)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant U as 用户
|
|
||||||
participant M as main.go
|
|
||||||
participant CP as GetContextPrompt
|
|
||||||
participant PicoMem as picoclaw MEMORY.md
|
|
||||||
participant AI as ProcessDirect
|
|
||||||
participant SC as SaveChat
|
|
||||||
participant DB as libSQL
|
|
||||||
participant VS as 向量服务
|
|
||||||
|
|
||||||
U->>M: 用户输入
|
|
||||||
M->>CP: GetContextPrompt(userInput)
|
|
||||||
CP->>PicoMem: 读取长期记忆
|
|
||||||
PicoMem-->>CP: 长期记忆内容
|
|
||||||
CP->>DB: 获取 currentSession
|
|
||||||
DB->>CP: session.Summary
|
|
||||||
|
|
||||||
Note over CP: 合并:长期记忆 + 会话摘要
|
|
||||||
|
|
||||||
CP->>M: 返回上下文
|
|
||||||
M->>AI: ProcessDirect(context + input)
|
|
||||||
AI->>U: 返回 AI 回复
|
|
||||||
|
|
||||||
U->>SC: SaveChat(userInput, aiReply)
|
|
||||||
SC->>DB: INSERT chat
|
|
||||||
SC->>DB: UPDATE session
|
|
||||||
|
|
||||||
Note over SC: 更新摘要和向量
|
|
||||||
|
|
||||||
SC->>VS: 异步生成向量
|
|
||||||
VS-->>SC: embedding
|
|
||||||
|
|
||||||
SC->>DB: 保存到 chats.summary_embedding
|
|
||||||
SC->>DB: 保存到 sessions.summary_embedding
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、四种查询场景
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
subgraph 查询场景
|
|
||||||
Q1[场景1: 历史摘要]
|
|
||||||
Q2[场景2: 话题检索]
|
|
||||||
Q3[场景3: 会话详情]
|
|
||||||
Q4[场景4: 会话内检索]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 触发条件
|
|
||||||
T1["之前聊过什么?"]
|
|
||||||
T2["谈论过 xxx?"]
|
|
||||||
T3["那次还说过什么?"]
|
|
||||||
T4["xxx 呢?"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 查询逻辑
|
|
||||||
L1[查 sessions 表]
|
|
||||||
L2[chats 向量检索<br/>Group By session_id]
|
|
||||||
L3[查 sessions 表<br/>WHERE id = ?]
|
|
||||||
L4[chats 向量检索<br/>WHERE session_id = ?]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 返回
|
|
||||||
R1[所有会话摘要]
|
|
||||||
R2[按 session 分组<br/>top5 摘要拼接]
|
|
||||||
R3[指定 session 摘要]
|
|
||||||
R4[同 session 内相关摘要]
|
|
||||||
end
|
|
||||||
|
|
||||||
T1 --> Q1
|
|
||||||
T2 --> Q2
|
|
||||||
T3 --> Q3
|
|
||||||
T4 --> Q4
|
|
||||||
|
|
||||||
Q1 --> L1
|
|
||||||
Q2 --> L2
|
|
||||||
Q3 --> L3
|
|
||||||
Q4 --> L4
|
|
||||||
|
|
||||||
L1 --> R1
|
|
||||||
L2 --> R2
|
|
||||||
L3 --> R3
|
|
||||||
L4 --> R4
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、三重检测机制
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
I[用户输入]
|
|
||||||
|
|
||||||
subgraph 检测层
|
|
||||||
D1[/recall 命令]
|
|
||||||
D2[关键词匹配]
|
|
||||||
D3[向量相似度]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 配置
|
|
||||||
C1[keywords]
|
|
||||||
C2[auto_recall]
|
|
||||||
C3[similarity_threshold]
|
|
||||||
end
|
|
||||||
|
|
||||||
I --> D1
|
|
||||||
I --> D2
|
|
||||||
I --> D3
|
|
||||||
|
|
||||||
D2 --> C1
|
|
||||||
D3 --> C3
|
|
||||||
|
|
||||||
C1 -->|匹配成功| R[强制 Recall]
|
|
||||||
D3 --> C2
|
|
||||||
|
|
||||||
C2 -->|开启| C3
|
|
||||||
C3 -->|阈值判断| R
|
|
||||||
|
|
||||||
D1 -->|触发| R
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、数据库表结构
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
sessions {
|
|
||||||
int id PK
|
|
||||||
string uuid
|
|
||||||
text summary
|
|
||||||
blob summary_embedding
|
|
||||||
string chat_ids
|
|
||||||
int created_at
|
|
||||||
int updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
chats {
|
|
||||||
int id PK
|
|
||||||
int session_id FK
|
|
||||||
text user_input
|
|
||||||
text ai_replies
|
|
||||||
text summary
|
|
||||||
blob summary_embedding
|
|
||||||
int created_at
|
|
||||||
int updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
sessions ||--o{ chats : "has many"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、上下文演变
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
subgraph 时间线
|
|
||||||
T1[开始]
|
|
||||||
T2[第1次对话]
|
|
||||||
T3[第2次对话]
|
|
||||||
T4[第N次对话]
|
|
||||||
T5[第1000次对话]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 上下文状态
|
|
||||||
S1[空]
|
|
||||||
S2[摘要1]
|
|
||||||
S3[摘要1+摘要2]
|
|
||||||
SN[摘要N]
|
|
||||||
S1000[摘要1000]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 实际状态
|
|
||||||
A1["context = 空 + 长期记忆"]
|
|
||||||
A2["context = 摘要1 + 长期记忆"]
|
|
||||||
A3["context = 摘要2 + 长期记忆"]
|
|
||||||
AN["context = 摘要N + 长期记忆"]
|
|
||||||
A1000["context = 摘要1000 + 长期记忆"]
|
|
||||||
end
|
|
||||||
|
|
||||||
T1 --> S1 --> A1
|
|
||||||
T2 --> S2 --> A2
|
|
||||||
T3 --> S3 -->|覆盖| A3
|
|
||||||
T4 --> SN -->|覆盖| AN
|
|
||||||
T5 --> S1000 -->|覆盖| A1000
|
|
||||||
|
|
||||||
Note over A1000: 始终只有1条会话摘要 + 长期记忆
|
|
||||||
```
|
|
||||||
|
|
||||||
**注意**:长期记忆来自 `~/.picoclaw/workspace/memory/MEMORY.md`,不受会话影响,会持续保留。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、完整流程图
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph 输入层
|
|
||||||
INPUT[用户输入]
|
|
||||||
CMD[/recall]
|
|
||||||
KW[关键词]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 检测层
|
|
||||||
CHECK{检测查询意图}
|
|
||||||
RECALL{Recall 触发?}
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 上下文构建
|
|
||||||
CONTEXT[上下文]
|
|
||||||
SESSION_SUM[当前 Session 摘要]
|
|
||||||
RECALL_RES[Recall 结果]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph AI 层
|
|
||||||
AI[ProcessDirect]
|
|
||||||
RESP[AI 回复]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 保存层
|
|
||||||
SAVE[SaveChat]
|
|
||||||
INSERT_CHAT[INSERT chat]
|
|
||||||
UPDATE_CHAT[UPDATE chat]
|
|
||||||
UPDATE_SESSION[UPDATE session]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 数据库
|
|
||||||
DB[(libSQL)]
|
|
||||||
SESSIONS[sessions 表]
|
|
||||||
CHATS[chats 表]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 向量服务
|
|
||||||
VS[向量服务]
|
|
||||||
EMB[Generate Embedding]
|
|
||||||
end
|
|
||||||
|
|
||||||
INPUT --> CHECK
|
|
||||||
CMD --> CHECK
|
|
||||||
KW --> CHECK
|
|
||||||
|
|
||||||
CHECK -->|普通对话| RECALL
|
|
||||||
CHECK -->|是查询| RECALL_RES
|
|
||||||
|
|
||||||
RECALL -->|否| SESSION_SUM
|
|
||||||
RECALL_RES --> CONTEXT
|
|
||||||
|
|
||||||
SESSION_SUM --> CONTEXT
|
|
||||||
CONTEXT --> AI
|
|
||||||
INPUT --> AI
|
|
||||||
|
|
||||||
AI --> RESP
|
|
||||||
RESP --> SAVE
|
|
||||||
|
|
||||||
SAVE --> INSERT_CHAT
|
|
||||||
INSERT_CHAT --> DB
|
|
||||||
DB --> CHATS
|
|
||||||
|
|
||||||
SAVE --> UPDATE_CHAT
|
|
||||||
UPDATE_CHAT --> DB
|
|
||||||
|
|
||||||
SAVE --> UPDATE_SESSION
|
|
||||||
UPDATE_SESSION --> DB
|
|
||||||
|
|
||||||
CHATS --> EMB
|
|
||||||
SESSIONS --> EMB
|
|
||||||
|
|
||||||
EMB --> VS
|
|
||||||
VS --> CHATS
|
|
||||||
VS --> SESSIONS
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 八、关键文件对应关系
|
|
||||||
|
|
||||||
| 模块 | 文件 | 职责 |
|
|
||||||
|------|------|------|
|
|
||||||
| 主流程 | `main.go` | 调用 GetContextPrompt、SaveChat |
|
|
||||||
| 配置 | `internal/config.go` | RecallConfig、VectorConfig |
|
|
||||||
| 数据库 | `internal/memory/db.go` | CRUD 操作 |
|
|
||||||
| 模型 | `internal/memory/model.go` | Session、Chat 结构体 |
|
|
||||||
| 向量 | `internal/memory/vector.go` | 硅基流动 API 调用 |
|
|
||||||
| 保存 | `internal/memory/save.go` | SaveChat、三重检测、长期记忆读取 |
|
|
||||||
| 查询 | `internal/memory/skill.go` | 4 个 Recall 函数 |
|
|
||||||
| 导出 | `internal/memory/export.go` | JSON 导出 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 九、双记忆系统合并
|
|
||||||
|
|
||||||
### 记忆来源
|
|
||||||
|
|
||||||
| 类型 | 来源 | 持久性 |
|
|
||||||
|------|------|--------|
|
|
||||||
| **长期记忆** | `~/.picoclaw/workspace/memory/MEMORY.md` | 跨会话,AI 自动更新 |
|
|
||||||
| **会话摘要** | `~/.config/hxclaw/hxclaw.db` sessions 表 | 当前会话,动态更新 |
|
|
||||||
| **聊天详情** | `~/.config/hxclaw/hxclaw.db` chats 表 | 所有历史,支持向量检索 |
|
|
||||||
|
|
||||||
### 上下文注入格式
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
=== 长期记忆 ===
|
|
||||||
(picoclaw MEMORY.md 内容)
|
|
||||||
|
|
||||||
=== 当前会话摘要 ===
|
|
||||||
(hxclaw sessions 表中的摘要)
|
|
||||||
|
|
||||||
用户新问题: xxx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 实现原理
|
|
||||||
|
|
||||||
`GetContextPrompt()` 函数在构建上下文时:
|
|
||||||
1. 读取 picoclaw 的 `MEMORY.md` 文件
|
|
||||||
2. 解析并提取有效内容(跳过 Markdown 标题)
|
|
||||||
3. 合并到上下文顶部
|
|
||||||
4. 追加会话摘要
|
|
||||||
5. 如有 recall 结果,继续追加
|
|
||||||
|
|
||||||
### 数据流向
|
|
||||||
|
|
||||||
```
|
|
||||||
picoclaw MEMORY.md ──┐
|
|
||||||
├──→ GetContextPrompt ──→ AI 上下文
|
|
||||||
hxclaw sessions ────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 十、配置项说明
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
memory:
|
|
||||||
recall:
|
|
||||||
keywords: ["之前", "聊过", "记得", "找找", "曾经", "谈论过", "提过"]
|
|
||||||
auto_recall: true # 自动相似度检测
|
|
||||||
similarity_threshold: 0.7 # 相似度阈值
|
|
||||||
max_results: 5 # 最大检索结果
|
|
||||||
|
|
||||||
vector:
|
|
||||||
max_search_results: 10 # 向量检索最大结果
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 十一、MongoDB 风格导出
|
|
||||||
|
|
||||||
### 导出文件
|
|
||||||
|
|
||||||
- 路径:`~/.config/hxclaw/export-data.json`
|
|
||||||
- 格式:每次退出时自动增量导出
|
|
||||||
|
|
||||||
### JSON 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"exported_at": "2026-04-27T06:12:22+08:00",
|
|
||||||
"sessions": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"uuid": "session-uuid",
|
|
||||||
"summary": "会话摘要...",
|
|
||||||
"chat_ids": [1, 2, 3],
|
|
||||||
"created_at": 1740000000,
|
|
||||||
"updated_at": 1740000000,
|
|
||||||
"chats": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"session_id": 1,
|
|
||||||
"user_input": "用户输入",
|
|
||||||
"ai_replies": ["AI回复1", "AI回复2"],
|
|
||||||
"summary": "摘要",
|
|
||||||
"created_at": 1740000000,
|
|
||||||
"updated_at": 1740000000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 设计特点
|
|
||||||
|
|
||||||
| 特性 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| 嵌套结构 | chats 嵌套在 sessions 下,类似 MongoDB 文档 |
|
|
||||||
| 增量导出 | 同 session 的 chats 累加,不重复创建 |
|
|
||||||
| UUID 匹配 | 按 UUID 判断是新建还是更新 |
|
|
||||||
| 版本控制 | version 字段支持格式演进 |
|
|
||||||
```
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
# hxclaw 聊天记忆体实现计划
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
hxclaw 聊天记忆体是一个基于 libSQL (TursoDB) 的对话上下文管理系统,用于替代 picoclaw 原有的 memory.md 文件,实现更灵活、可控的会话历史管理。
|
|
||||||
|
|
||||||
**注意**:本计划文档已实现完成,详细功能请参考 `docs/discussion.md`。
|
|
||||||
|
|
||||||
## 技术选型
|
|
||||||
|
|
||||||
| 组件 | 选型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| 数据库 | libSQL (嵌入式) | 本地文件数据库 `~/.config/hxclaw/hxclaw.db` |
|
|
||||||
| 向量模型 | BAAI/bge-m3 | 1024 维,硅基流动 API |
|
|
||||||
| UUID | github.com/google/uuid | 生成唯一会话 ID |
|
|
||||||
| 第三方库 | go_modules/ | 本地化方案解决网络问题 |
|
|
||||||
|
|
||||||
## 数据库设计
|
|
||||||
|
|
||||||
### sessions 表
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
uuid TEXT UNIQUE NOT NULL,
|
|
||||||
summary TEXT,
|
|
||||||
summary_embedding BLOB,
|
|
||||||
chat_ids TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### chats 表
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS chats (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
session_id INTEGER NOT NULL,
|
|
||||||
user_input TEXT NOT NULL,
|
|
||||||
ai_replies TEXT,
|
|
||||||
summary TEXT,
|
|
||||||
summary_embedding BLOB,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心流程
|
|
||||||
|
|
||||||
1. **Session 管理**:自动创建(首次输入)+ 手动创建(`/new`)
|
|
||||||
2. **消息保存**:在 AI 返回后调用 `SaveChat()` 保存到数据库
|
|
||||||
3. **查询命令**:`/memory`, `/sessions`, `/new`
|
|
||||||
|
|
||||||
## 配置
|
|
||||||
|
|
||||||
- **启用**:默认启用
|
|
||||||
- **数据库路径**:`~/.config/hxclaw/hxclaw.db`
|
|
||||||
- **用户配置**:`~/.config/hxclaw/config.yml`
|
|
||||||
|
|
||||||
## 实现状态
|
|
||||||
|
|
||||||
| 功能 | 状态 |
|
|
||||||
|------|------|
|
|
||||||
| 数据库 CRUD | ✅ 完成 |
|
|
||||||
| 聊天记录保存 | ✅ 完成 |
|
|
||||||
| 命令支持 | ✅ 完成 |
|
|
||||||
| JSON 导出 | ✅ 完成 |
|
|
||||||
| 向量服务 | ⚠️ 框架完成,待完善 |
|
|
||||||
| 向量查询 | ⏳ 待实现 |
|
|
||||||
|
|
||||||
hxclaw 聊天记忆体是一个基于 TursoDB (libSQL) 的对话上下文管理系统,用于替代 picoclaw 原有的 memory.md 文件,实现更灵活、可控的会话历史管理。
|
|
||||||
|
|
||||||
核心目标:
|
|
||||||
1. 保留 picoclaw 的 memory.md 作为 AI 长期记忆
|
|
||||||
2. 新增对话记忆体作为会话维度的短期记忆
|
|
||||||
3. 支持向量检索和多场景查询
|
|
||||||
|
|
||||||
## 技术选型
|
|
||||||
|
|
||||||
| 组件 | 选型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| 数据库 | libSQL (嵌入式) | 本地文件数据库,支持 JSON 导出 |
|
|
||||||
| 向量模型 | BAAI/bge-m3 | 1024 维,硅基流动 API |
|
|
||||||
| UUID | github.com/google/uuid | 生成唯一会话 ID |
|
|
||||||
| libSQL 驱动 | github.com/tursodatabase/go-libsql | CGO 需启用 |
|
|
||||||
|
|
||||||
## 数据库设计
|
|
||||||
|
|
||||||
### 表结构
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- sessions 表(会话维度)
|
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
uuid TEXT UNIQUE NOT NULL, -- 外键引用用 UUID
|
|
||||||
summary TEXT, -- 会话压缩后的摘要
|
|
||||||
summary_embedding BLOB, -- 1024维向量(二进制)
|
|
||||||
chat_ids TEXT, -- JSON数组 ["id1","id2",...]
|
|
||||||
created_at INTEGER NOT NULL, -- Unix时间戳
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- chats 表(消息维度)
|
|
||||||
CREATE TABLE IF NOT EXISTS chats (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
session_id INTEGER NOT NULL, -- 外键到 sessions.id
|
|
||||||
user_input TEXT NOT NULL, -- 用户输入
|
|
||||||
ai_replies TEXT, -- JSON数组 [{"role":"assistant","content":"..."},...]
|
|
||||||
summary TEXT, -- 单条消息摘要
|
|
||||||
summary_embedding BLOB, -- 1024维向量
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 索引
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chats_session_id ON chats(session_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_uuid ON sessions(uuid);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 数据模型
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Session 会话记录
|
|
||||||
type Session struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UUID string `json:"uuid"`
|
|
||||||
Summary string `json:"summary"`
|
|
||||||
SummaryEmbedding []byte `json:"-"`
|
|
||||||
ChatIDs []int64 `json:"chat_ids"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chat 聊天记录
|
|
||||||
type Chat struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SessionID int64 `json:"session_id"`
|
|
||||||
UserInput string `json:"user_input"`
|
|
||||||
AIReplies []string `json:"ai_replies"`
|
|
||||||
Summary string `json:"summary"`
|
|
||||||
SummaryEmbedding []byte `json:"-"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心流程
|
|
||||||
|
|
||||||
### 1. Session 管理
|
|
||||||
|
|
||||||
#### 自动开始(模式 A)
|
|
||||||
- 用户首次输入内容时,检查是否存在活跃 Session
|
|
||||||
- 若无,则自动创建新 Session
|
|
||||||
|
|
||||||
#### 手动开始(模式 B)
|
|
||||||
- 用户输入 `/new` 命令,强制创建新 Session
|
|
||||||
|
|
||||||
### 2. 消息处理流程
|
|
||||||
|
|
||||||
```
|
|
||||||
用户输入
|
|
||||||
↓
|
|
||||||
[可选] 创建新 Session(首次输入时)
|
|
||||||
↓
|
|
||||||
chats 表插入记录 (session_id, user_input, created_at)
|
|
||||||
↓
|
|
||||||
AI 处理
|
|
||||||
↓
|
|
||||||
AI 返回后:
|
|
||||||
1. 生成摘要 + 向量
|
|
||||||
2. chats 表更新 (ai_replies, summary, summary_embedding)
|
|
||||||
3. sessions 表更新 (chat_ids 追加, summary 压缩, summary_embedding 更新)
|
|
||||||
↓
|
|
||||||
将 Session 摘要作为上下文注入 AI
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 上下文注入
|
|
||||||
|
|
||||||
```
|
|
||||||
=== 当前会话摘要 ===
|
|
||||||
[最新摘要内容]
|
|
||||||
===
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 查询场景(4种)
|
|
||||||
|
|
||||||
| 场景 | 用户意图 | 查询方式 |
|
|
||||||
|------|----------|----------|
|
|
||||||
| 1 | "之前聊过什么" | 查询最新 Session 的 summary |
|
|
||||||
| 2 | "某某某相关内容" | 向量搜索 top 5 |
|
|
||||||
| 3 | "某次谈论时的详情" | 定位 Session ID,返回 summary |
|
|
||||||
| 4 | 同 Session 内其他相关内容 | session_id 过滤 + 向量搜索 |
|
|
||||||
|
|
||||||
## 模块设计
|
|
||||||
|
|
||||||
### 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
cmd/hxclaw/internal/memory/
|
|
||||||
├── db.go # 数据库初始化和 CRUD 操作
|
|
||||||
├── model.go # 数据模型定义
|
|
||||||
├── vector.go # 向量服务(硅基流动 API)
|
|
||||||
├── skill.go # 内置 Skill(查询逻辑)
|
|
||||||
└── export.go # 导出功能
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置项
|
|
||||||
|
|
||||||
在 `project.config.yml` 中新增:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# 聊天记忆体配置
|
|
||||||
memory:
|
|
||||||
enabled: true # 启用开关
|
|
||||||
db_path: "./hxclaw.db" # 数据库路径
|
|
||||||
auto_session: true # 自动创建 Session
|
|
||||||
export_on_exit: true # 退出时导出
|
|
||||||
|
|
||||||
# 向量服务配置
|
|
||||||
vector:
|
|
||||||
api_key: "sk-xxx" # 硅基流动 API Key
|
|
||||||
base_url: "https://api.siliconflow.cn/v1"
|
|
||||||
model: "BAAI/bge-m3" # 向量模型
|
|
||||||
dimensions: 1024 # 向量维度
|
|
||||||
```
|
|
||||||
|
|
||||||
## 实现步骤
|
|
||||||
|
|
||||||
### 阶段一:基础设施
|
|
||||||
1. 新增依赖并测试连接(go-libsql, google/uuid)
|
|
||||||
2. 创建 `memory/model.go` - 数据模型
|
|
||||||
3. 创建 `memory/db.go` - CRUD 操作
|
|
||||||
|
|
||||||
### 阶段二:向量服务
|
|
||||||
4. 创建 `memory/vector.go` - 硅基流动 Embedding API
|
|
||||||
|
|
||||||
### 阶段三:核心逻辑
|
|
||||||
5. 修改 `main.go` - 集成 Session
|
|
||||||
6. 修改消息处理流程 - 存储和摘要
|
|
||||||
7. 创建 `memory/skill.go` - 查询逻辑
|
|
||||||
|
|
||||||
### 阶段四:UI 和导出
|
|
||||||
8. 添加命令支持(`/memory`, `/sessions`)
|
|
||||||
9. 创建 `memory/export.go` - JSON 导出
|
|
||||||
|
|
||||||
## 依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get github.com/tursodatabase/go-libsql
|
|
||||||
go get github.com/google/uuid
|
|
||||||
go get github.com/sjzsdu/langchaingo-cn/llms/siliconflow
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **CGO 要求**:`go-libsql` 需要 CGO 启用
|
|
||||||
2. **向量维度**:BGE-M3 默认 1024 维
|
|
||||||
3. **摘要生成**:使用 AI 返回内容的前 N 字符作为摘要
|
|
||||||
4. **错误处理**:向量服务失败时降级为纯文本搜索
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
# picoclaw 技术研究报告
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本报告基于对 picoclaw v0.2.6 源代码的研究,详细分析其核心机制。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 系统架构
|
|
||||||
|
|
||||||
### 1.1 核心模块
|
|
||||||
|
|
||||||
```
|
|
||||||
picoclaw/
|
|
||||||
├── pkg/
|
|
||||||
│ ├── agent/ # AI 代理核心逻辑
|
|
||||||
│ ├── providers # LLM 提供者
|
|
||||||
│ ├── config/ # 配置管理
|
|
||||||
│ ├── tools/ # 工具注册与执行
|
|
||||||
│ ├── skills/ # Skill 加载器
|
|
||||||
│ ├── bus/ # 消息总线
|
|
||||||
│ └── channels/ # 消息通道
|
|
||||||
└── web/ # Web 界面
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 依赖关系
|
|
||||||
|
|
||||||
- **agent**:核心模块,依赖 providers、tools、skills、bus
|
|
||||||
- **providers**:LLM API 调用(OpenAI、Anthropic 等)
|
|
||||||
- **tools**:工具执行(exec、read_file、web_fetch 等)
|
|
||||||
- **skills**:通过 SKILL.md 加载技能定义
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Skill 机制
|
|
||||||
|
|
||||||
### 2.1 加载机制
|
|
||||||
|
|
||||||
**位置**:`pkg/skills/loader.go`
|
|
||||||
|
|
||||||
Skill 存储在三个位置,优先级:
|
|
||||||
1. `workspace/skills/` - 项目级
|
|
||||||
2. `~/.picoclaw/skills/` - 全局
|
|
||||||
3. 内置 skills 目录
|
|
||||||
|
|
||||||
每个 skill 必须是 `SKILL.md` 文件,包含:
|
|
||||||
- Frontmatter (YAML/JSON) 定义 `name` 和 `description`
|
|
||||||
- Markdown 主体内容(操作指导)
|
|
||||||
|
|
||||||
### 2.2 执行机制
|
|
||||||
|
|
||||||
**关键发现**:Skill 本质是 Markdown 文档,不是可执行代码
|
|
||||||
|
|
||||||
AI 阅读 SKILL.md 后,调用实际工具执行任务:
|
|
||||||
```
|
|
||||||
加载 SKILL.md → AI 阅读 → 调用工具 → 执行结果
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 工具执行机制
|
|
||||||
|
|
||||||
### 3.1 执行流程
|
|
||||||
|
|
||||||
**位置**:`pkg/agent/loop.go:runTurn` (约 2332-2899 行)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 串行执行每个工具调用
|
|
||||||
for i, tc := range normalizedToolCalls {
|
|
||||||
toolName := tc.Name
|
|
||||||
toolArgs := cloneStringAnyMap(tc.Arguments)
|
|
||||||
|
|
||||||
// 执行工具
|
|
||||||
toolResult := ts.agent.Tools.ExecuteWithContext(
|
|
||||||
execCtx, toolName, toolArgs, ts.channel, ts.chatID, asyncCallback,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 结果添加到消息历史
|
|
||||||
toolResultMsg := providers.Message{
|
|
||||||
Role: "tool",
|
|
||||||
Content: contentForLLM,
|
|
||||||
ToolCallID: toolCallID,
|
|
||||||
}
|
|
||||||
messages = append(messages, toolResultMsg)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 关键特性
|
|
||||||
|
|
||||||
| 特性 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| 执行方式 | **串行执行**,非并行 |
|
|
||||||
| 结果收集 | 通过 `providers.Message` 添加到上下文 |
|
|
||||||
| 用户显示 | `ForUser` 字段非空时发送给用户 |
|
|
||||||
| 静默结果 | `SilentResult()` 不显示给用户,但发给 LLM |
|
|
||||||
|
|
||||||
### 3.3 工具结果标志
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Silent: 不发送消息给用户,但发送给 LLM
|
|
||||||
Silent bool `json:"silent"`
|
|
||||||
|
|
||||||
// ResponseHandled: 工具已处理响应,不生成独立消息
|
|
||||||
ResponseHandled bool `json:"response_handled,omitempty"`
|
|
||||||
|
|
||||||
// IsError: 标记为错误结果
|
|
||||||
IsError bool `json:"is_error,omitempty"`
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 消息处理
|
|
||||||
|
|
||||||
### 4.1 消息总线
|
|
||||||
|
|
||||||
**位置**:`pkg/bus/bus.go`
|
|
||||||
|
|
||||||
```
|
|
||||||
用户输入 → AgentLoop.ProcessDirect() → AI 处理 → 工具执行 ←
|
|
||||||
↓
|
|
||||||
bus.PublishOutbound() → 输出
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 消息类型
|
|
||||||
|
|
||||||
| 类型 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| InboundMessage | 用户输入 |
|
|
||||||
| OutboundMessage | AI 输出 |
|
|
||||||
| ToolResult | 工具结果 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 异步执行
|
|
||||||
|
|
||||||
### 5.1 AsyncExecutor 接口
|
|
||||||
|
|
||||||
**位置**:`pkg/tools/base.go:107`
|
|
||||||
|
|
||||||
```go
|
|
||||||
type AsyncExecutor interface {
|
|
||||||
Tool
|
|
||||||
ExecuteAsync(ctx context.Context, args map[string]any, cb AsyncCallback) *ToolResult
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 使用方式
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 执行时传入回调函数
|
|
||||||
toolResult := ts.agent.Tools.ExecuteWithContext(
|
|
||||||
execCtx, toolName, toolArgs, ts.channel, ts.chatID,
|
|
||||||
asyncCallback, // 异步回调
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. TTS 工具分析
|
|
||||||
|
|
||||||
### 6.1 文件位置
|
|
||||||
|
|
||||||
- 主文件:`pkg/tools/tts_send.go`
|
|
||||||
- TTS 提供者:`pkg/audio/tts/tts.go`
|
|
||||||
|
|
||||||
### 6.2 实现方式
|
|
||||||
|
|
||||||
1. 通过 `SendTTSTool` 执行 TTS 合成
|
|
||||||
2. 调用 `tts.SynthesizeAndStore()` 生成音频
|
|
||||||
3. 返回文件路径或直接播放
|
|
||||||
|
|
||||||
### 6.3 已知问题
|
|
||||||
|
|
||||||
| 问题 | 原因 |
|
|
||||||
|------|------|
|
|
||||||
| 播放内容不一致 | 异步回调竞争 |
|
|
||||||
| 结果不显示 | 使用了 `SilentResult()` |
|
|
||||||
| 多条记录只显示一条 | 串行执行中的状态竞争 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 配置系统
|
|
||||||
|
|
||||||
### 7.1 配置文件
|
|
||||||
|
|
||||||
- 项目级:`config.yaml`
|
|
||||||
- 用户级:`~/.picoclaw/config.json`
|
|
||||||
- 环境变量:`PICOCLAW_*` 前缀
|
|
||||||
|
|
||||||
### 7.2 配置结构
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Config struct {
|
|
||||||
Agents AgentsConfig `json:"agents"`
|
|
||||||
Tools ToolsConfig `json:"tools"`
|
|
||||||
Channels ChannelsConfig `json:"channels"`
|
|
||||||
Voice VoiceConfig `json:"voice"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. hxclaw 集成方式
|
|
||||||
|
|
||||||
### 8.1 复用策略
|
|
||||||
|
|
||||||
hxclaw 通过 Go replace 机制复用 picoclaw:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// go.mod
|
|
||||||
replace github.com/sipeed/picoclaw => ./path/to/picoclaw
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 核心依赖
|
|
||||||
|
|
||||||
- `pkg/agent` - AI 代理核心
|
|
||||||
- `pkg/providers` - LLM 提供者
|
|
||||||
- `pkg/config` - 配置加载
|
|
||||||
- `pkg/bus` - 消息总线
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 总结
|
|
||||||
|
|
||||||
| 模块 | 机制 | 阻塞 |
|
|
||||||
|------|------|------|
|
|
||||||
| Skill | Markdown 文档 | 否 |
|
|
||||||
| 工具 | 串行执行 | 是 |
|
|
||||||
| 异步工具 | 回调机制 | 可选 |
|
|
||||||
| 消息总线 | 非阻塞 | 否 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 建议
|
|
||||||
|
|
||||||
1. **避免阻塞**:长时间任务使用 AsyncExecutor
|
|
||||||
2. **结果显示**:检查 Silent/ResponseHandled 标志
|
|
||||||
3. **并发控制**:使用 SubTurn 的 concurrencySem
|
|
||||||
13
go.mod
13
go.mod
@@ -6,24 +6,17 @@ require (
|
|||||||
charm.land/bubbles/v2 v2.1.0
|
charm.land/bubbles/v2 v2.1.0
|
||||||
charm.land/glamour/v2 v2.0.0
|
charm.land/glamour/v2 v2.0.0
|
||||||
charm.land/lipgloss/v2 v2.0.2
|
charm.land/lipgloss/v2 v2.0.2
|
||||||
github.com/charmbracelet/x/term v0.2.2
|
|
||||||
github.com/ergochat/readline v0.1.3
|
github.com/ergochat/readline v0.1.3
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/sipeed/picoclaw v0.2.6
|
github.com/sipeed/picoclaw v0.2.6
|
||||||
github.com/tursodatabase/libsql-client-go v0.0.0-00010101000000-000000000000
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
// 使用本地第三方库
|
|
||||||
replace github.com/tursodatabase/libsql-client-go => ./go_modules/libsql-client-go
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||||
github.com/adhocore/gronx v1.19.6 // indirect
|
github.com/adhocore/gronx v1.19.6 // indirect
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||||
github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect
|
github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect
|
||||||
@@ -47,11 +40,11 @@ require (
|
|||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
|
||||||
github.com/creack/pty v1.1.24 // indirect
|
github.com/creack/pty v1.1.24 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -60,6 +53,7 @@ require (
|
|||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect
|
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect
|
||||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/h2non/filetype v1.1.3 // indirect
|
github.com/h2non/filetype v1.1.3 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
@@ -92,7 +86,6 @@ require (
|
|||||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
golang.org/x/crypto v0.49.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
@@ -103,5 +96,5 @@ require (
|
|||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.48.0 // indirect
|
modernc.org/sqlite v1.48.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -16,8 +16,6 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
|
|||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||||
@@ -78,8 +76,6 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
|
|||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -199,8 +195,8 @@ golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
|||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
@@ -216,8 +212,8 @@ golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
|||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -247,8 +243,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
|
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
||||||
modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
Submodule go_modules/libsql-client-go deleted from 236aa1ff8a
@@ -1,33 +1,17 @@
|
|||||||
# hxclaw 项目配置文件
|
# hxclaw 项目配置文件
|
||||||
|
# 项目级配置,会覆盖用户配置
|
||||||
|
|
||||||
# 模拟流式输出配置
|
# 模拟流式输出配置
|
||||||
streaming:
|
streaming:
|
||||||
line_delay_ms: 1000
|
line_delay_ms: 1000 # 每行输出后的延迟(毫秒)
|
||||||
last_line_delay_ms: 600
|
last_line_delay_ms: 600 # 最后一行延迟(毫秒)
|
||||||
|
|
||||||
# Markdown 渲染配置
|
# Markdown 渲染配置
|
||||||
markdown:
|
markdown:
|
||||||
theme: dark
|
theme: light # 渲染主题:dark, light, dracula, tokyo-night 等
|
||||||
line_width: -1
|
line_width: 100 # 自动换行宽度(0=自动,>0=固定宽度,-1=禁用)
|
||||||
|
|
||||||
# UI 配置
|
# UI 配置
|
||||||
ui:
|
ui:
|
||||||
logo: "🦐"
|
logo: "🦐"
|
||||||
user_icon: "👀 "
|
user_icon: "👀 "
|
||||||
|
|
||||||
# TTS 语音配置
|
|
||||||
tts:
|
|
||||||
enabled: false
|
|
||||||
port: 9876
|
|
||||||
auto: true
|
|
||||||
|
|
||||||
# 聊天记忆体配置
|
|
||||||
memory:
|
|
||||||
enabled: false
|
|
||||||
auto_session: true
|
|
||||||
auto_export: true
|
|
||||||
vector:
|
|
||||||
base_url: "https://api.siliconflow.cn/v1"
|
|
||||||
model: "BAAI/bge-m3"
|
|
||||||
dimension: 1024
|
|
||||||
max_search_results: 10
|
|
||||||
190
taolun.md
190
taolun.md
@@ -46,8 +46,8 @@ hxclaw/
|
|||||||
### 6. Markdown 终端渲染
|
### 6. Markdown 终端渲染
|
||||||
|
|
||||||
- 使用 charmbracelet 家族库
|
- 使用 charmbracelet 家族库
|
||||||
- glamour:Markdown 渲染(自带代码高亮)
|
|
||||||
- lipgloss:终端样式
|
- lipgloss:终端样式
|
||||||
|
- glow:代码高亮
|
||||||
- 流程:Markdown → ANSI 转义序列 → 终端显示
|
- 流程:Markdown → ANSI 转义序列 → 终端显示
|
||||||
|
|
||||||
### 7. 部署方式
|
### 7. 部署方式
|
||||||
@@ -326,6 +326,7 @@ func outputLineByLine(text string) {
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# hxclaw 项目配置文件
|
# hxclaw 项目配置文件
|
||||||
|
# 项目级配置,会覆盖用户配置
|
||||||
|
|
||||||
# 模拟流式输出配置
|
# 模拟流式输出配置
|
||||||
streaming:
|
streaming:
|
||||||
@@ -334,25 +335,21 @@ streaming:
|
|||||||
|
|
||||||
# Markdown 渲染配置
|
# Markdown 渲染配置
|
||||||
markdown:
|
markdown:
|
||||||
glamour_style: dark # 渲染主题:dark, light, dracula, tokyo-night 等
|
theme: dark # 渲染主题:dark, light, dracula, tokyo-night 等
|
||||||
wrap_width: 0 # 自动换行宽度(0=自动获取终端宽度)
|
line_width: 0 # 自动换行宽度(0=自动获取终端宽度)
|
||||||
|
|
||||||
# UI 配置
|
# UI 配置
|
||||||
ui:
|
ui:
|
||||||
logo: "🦐" # Logo
|
logo: "🦐" # Logo
|
||||||
user_prefix: "👀 " # 用户输入前缀
|
user_icon: "👀 " # 用户输入提示符
|
||||||
|
|
||||||
# TTS 语音配置
|
|
||||||
tts:
|
|
||||||
enabled: false # 全局开关(默认关闭)
|
|
||||||
port: 9876 # daemon 端口
|
|
||||||
auto: true # AI 回复后自动朗读
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 配置加载优先级
|
#### 配置加载优先级
|
||||||
|
|
||||||
1. 环境变量 `HXCLAW_CONFIG` 指定路径
|
1. 用户配置 `~/.config/hxclaw/config.yml`
|
||||||
2. 项目根目录 `project.config.yml`
|
2. 环境变量 `HXCLAW_CONFIG` 指定路径
|
||||||
|
3. 项目根目录 `project.config.yml`
|
||||||
|
4. 代码中的默认值
|
||||||
|
|
||||||
#### 代码实现
|
#### 代码实现
|
||||||
|
|
||||||
@@ -388,172 +385,3 @@ func getConfigPath() string {
|
|||||||
- 人眼需要约 30-50ms 才能感知单次视觉变化
|
- 人眼需要约 30-50ms 才能感知单次视觉变化
|
||||||
- 空白字符不应逐个输出,应批量处理
|
- 空白字符不应逐个输出,应批量处理
|
||||||
- 终端宽度 100% 时 Markdown 渲染会显著增加行数和字符数
|
- 终端宽度 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)`,便于错误处理
|
|
||||||
- 状态文字单独使用颜色样式
|
|
||||||
- 失败时显示"会话保存异常"(暗红色)
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user