Files
YunShu/docs/changelog.md
titor c4a0e3ef53 feat: v2.3.0 流式输出 + 日志系统 + 会议室架构全面升级
- 流式输出: SSE 逐 token 接收, \\n\n\ 段落缓冲后 mdprint 彩色渲染
- 日志系统: charmbracelet/log v2 双写(stderr + log.yml), yunshu log 命令
- 会议室架构: dialog(main) + weather/profile/note(sub) 多 Agent 编排
- 泛型工具注册: NewTool[T] 反射推导 JSON Schema, 类型安全
- 安全加固: safeMemoryPath 三段校验(EvalSymlinks+Rel), maxToolCalls=2
- 性能优化: sync.Once 延迟加载, note 一步完成, obs/summary 合并
- Prompt 适配: 流式输出原则(先调工具不说话), 单 Agent 查询跳过 obs+summary
- 文档: AGENTS.md + architecture.md + changelog.md 全部同步至 v2.3.0
2026-05-16 17:21:29 +08:00

477 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 云枢·Agent 版本变更日志
> 坐看云卷云舒,静听花开花落
## [2.3.0] - 2026-05-16
### 日志系统 + 性能优化 + 安全加固
#### 日志系统
引入 `charmbracelet/log` v2 作为结构化日志库,取代零散的 `log.Printf` / `fmt.Fprintln(os.Stderr, ...)`
| 组件 | 说明 |
|------|------|
| `logger.go` | charmbracelet/log 全局实例,输出到 stderr |
| `log.go` | log.yml 持久化YAML 序列追加)、双写 wrapper、`yunshu log` 命令 |
日志流向:
```
warnLog/errorLog/infoLog
├─ logToStderr ? → charmbracelet/log → stderr交互模式默认关闭/log on 开启)
└─ 始终写入 → appendLog → log.ymlYAML 序列,位于 ~/.config/yunshu/log.yml
```
新增日志点:
```
[INFO] task(weather) 开始
[INFO] 子 Agent 开始 agent=weather
[INFO] LLM 调用完成 tokens=1420 duration=3.2s
[INFO] task(weather) 完成
[WARN] 解析缓存失败 agent=weather err=...
```
`yunshu log` 子命令:
```
yunshu log → 全量显示(时间倒序)
yunshu log --top N → 只看最后 N 条
yunshu log --level warn → 过滤级别
yunshu log --clear → 清空
yunshu log --watch → 监听模式2s 轮询)
```
#### 流式输出
LLM 响应改为 SSE 流式输出,按 `\n\n` 段落边界缓冲后经 mdprint 渲染到 stdout
| 组件 | 文件 | 说明 |
|------|------|------|
| SSE 类型 | `llm.go` | `sseChunk`/`sseDelta`/`sseToolCallDelta` 等 |
| `CallLLMStream` | `llm.go` | 请求加 `stream: true``bufio.Reader` 逐行解析 |
| 段落缓冲 | `llm.go` | `blockBuf` + `tryFlushBlocks`:检测最后一个 `\n\n`,完成块过 mdprint残段续传 |
| 流结束刷残段 | `llm.go` | 末尾 `mdprint.Print(blockBuf)` 兜底 |
| 重建响应 | `llm.go` | tool_call 按 index 累积 + SSE 碎片合并 |
关键设计:
```
LLM SSE → blockBuf += content
tryFlushBlocks:
├─ 有 \n\n → 之前的部分 mdprint.Print(complete)
│ → 剩余部分留在 blockBuf
└─ 无 \n\n → stay
流结束 → mdprint.Print(blockBuf)
```
Prompt 适配(`agents/dialog-agent.md`
- 新增"流式输出原则":先调工具,不要先说话
- 调度表 `回应 → task(profile)` 改为 `静默调 task拿到结果后再回应`
#### 性能优化
| 优化 | 改动 | 效果 |
|------|------|------|
| note-sub 一步完成 | prompt 收紧read→write→立即返回 | note 保存从 ~50s 降至 ~10s |
| dialog 合并 obs+summary | 单 Agent 查询跳过观察和摘要;综合查询合并为一轮 | 节省 ~30s |
| maxToolCalls=2 | RunSubAgent 超限兜底 | 防止死循环 |
#### 代码加固
| 改动 | 文件 | 说明 |
|------|------|------|
| LLM 延迟加载 | `llm.go` | `init()``sync.Once``--help` 不再读 config |
| 路径安全检查 | `tool.go` | `safeMemoryPath()`Clean + EvalSymlinks + Rel 三段校验 |
| 静默错误 → warnLog | 8 处 across runtime/tool/registry | 所有 `json.Unmarshal` / `yaml.Unmarshal` 吞掉的错误改为结构化日志 |
| 对话记忆修剪 | `runtime.go` | `LoadSession` 只返回最近 40 条消息 |
| Onboard 补齐 | `onboard.go` | `ensureUserConfig()` 创建 user.md/soul.md/notes/ |
| 热加载 | `main.go` | 交互模式每轮 `ScanAgents()`,新增 agent 即时生效 |
#### UX 改进
- 交互模式默认不显示日志(`/log on` 开启)
- `--help` 移除环境变量章节,增加 `log` 命令说明
- 单次查询模式保留日志显示
---
## [2.2.0] - 2026-05-16
### 存储重组session 目录 + .yml 统一后缀
#### 目录结构调整
| 旧路径 | 新路径 | 原因 |
|--------|--------|------|
| `session.json` | `session/session.json` | 对话会话文件归入 session 目录 |
| `context/dialog.yaml` | `session/dialog.yml` | context 语义模糊,与 session 合并;.yml 后缀 |
| `config.yaml` | `config.yml` | 统一后缀 |
| `log.yaml` | `log.yml` | 统一后缀 |
| `context/` | 已删除 | 文件移至 session/ |
所有 yaml 文件统一使用 `.yml` 后缀。配置文件、对话摘要、日志文件全部一致性调整。
#### 代码改动
| 文件 | 改动 |
|------|------|
| `runtime.go` | `sessionPath()` 改为 `session/session.json` |
| `config.go` | `LoadConfig`/`SaveConfig` 读写 `config.yml` |
| `onboard.go` | 提示信息改为 `config.yml` |
| `main.go` | 新增 `migrateFilePaths()` 处理所有旧路径迁移 |
| `tool.go` | 描述字符串更新 `context/``session/``.yaml``.yml` |
| `agents/dialog-agent.md` | 所有 `context/dialog.yaml``session/dialog.yml` |
| `docs/` | 存储树、写入策略、编码规范全部同步更新 |
#### 迁移逻辑
`migrateFilePaths()` 在启动时自动处理:
- `config.yaml` → 复制到 `config.yml`,删除旧文件
- `session.json` → 复制到 `session/session.json`,删除旧文件
- `context/dialog.yaml` → 复制到 `session/dialog.yml`,删除旧文件及空目录
- `log.yaml` → 复制到 `log.yml`,删除旧文件
所有迁移仅在目标文件不存在时执行,支持幂等。
---
## [2.1.0] - 2026-05-16
### 用户画像 + 备忘录系统
#### 拆分 memory.json
旧的 `memory.json` 一锅烩,现在拆成**按用途分文件**
| 旧文件 | 新文件 | 格式 | 维护者 |
|--------|--------|------|--------|
| `memory.json["personality"]` | `config/soul.md` | Markdown | 用户手动编辑 |
| `memory.json["dialog_context"]` | `context/dialog.yaml` | YAML | dialog 每轮写入 |
| `memory.json["agent_errors"]` | `log.yaml` | YAML | 系统追加 |
| (不存在) | `config/user.md` | Markdown | profile-sub 维护 |
| (不存在) | `notes.md` + `notes/*.md` | Markdown | note-sub 维护 |
迁移逻辑在 `main.go:migrateMemoryJSON()`,启动时自动检测并迁移,确认完成后再删除 `memory.json`
#### 新增子 Agent
| Agent | 文件 | 用途 |
|-------|------|------|
| profile-sub | `agents/profile-sub.md` | 从对话中提取用户画像,增量合并到 `config/user.md` |
| note-sub | `agents/note-sub.md` | 笔记管理。默认 `notes.md` 列表,复杂内容可存为 `notes/{name}.md` |
#### memory.read/write 工具改造
从旧的 flat JSON key-value 改为路径路由:
- `.md` 文件 → 全文覆写(`memory.write("config/user.md", markdown_str)`
- `.yaml` 文件 → 合并写入(`memory.write("context/dialog.yaml", {topic, last_agent})`
- 目录 → 返回文件列表
- 安全检查:拦截 `..` 遍历和绝对路径
- 目录自动创建
#### dialog-agent.md 更新
- 新增调度规则:检测用户透露个人信息 → 调 `task("profile", ...)` 提取画像
- 新增调度规则:检测"帮我记住" → 调 `task("note", ...)` 存备忘录
- 对话摘要改为写入 `context/dialog.yaml`,而非旧 `memory.json["dialog_context"]`
- 用户画像改为读取 `config/user.md`
- 新增观察规则:每轮回复后向 `## AI观察到` 段写入语气/情绪/性格观察
#### heading-aware merge
`memory.write``.md` 文件由全文覆写改为按 `##` 标题合并:
- `memory.write("config/user.md", "## 画像\n...")` 只替换 `## 画像` 段,`## AI观察到` 段不受影响
- 同一 writer 可多次写入同一标题,每次覆盖该段内容
- 不同 writer 写不同标题,互不干扰
- 底层函数 `mdMerge()``##` 拆分 → 标题匹配 → 重组
#### profile-sub.md 更新
- 改为写 `## 画像` 段(不再写整篇 user.md
- 文档规范化:给出 `## 画像` 格式示例
- 强调不破坏其他段(`## AI观察到` 等)
---
### 发布会:会议室架构核心引擎就绪
**架构规划:会议室模式**(阶段一全部完成,超计划交付)
#### 核心引擎
| 组件 | 文件 | 说明 |
|------|------|------|
| Agent 注册中心 | `registry.go` | `ScanAgents()``type`(main/sub) 分类,用户目录覆盖 |
| 子 Agent 隔离运行 | `runtime.go`: `RunSubAgent` | 隔离 LLM 循环,返回 `---RESULT---`/`---TEXT---` |
| 主 Agent 循环 | `runtime.go`: `RunAgent` | session 持久化 + mdprint 渲染 |
| 缓存系统 | `runtime.go`: cache 辅助 | SHA256[:6] 拼 key惰性过期 |
| 工具目录生成 | `catalog.go` | `GenerateToolsYAML``BuildSubAgentPrompt` |
#### 新增工具
| 工具 | 注册方式 | 说明 |
|------|---------|------|
| `task` | `NewTool[TaskInput]` | 调度子 Agent + 缓存管理 |
| `memory.read` | `NewTool[MemoryReadInput]` | 读 `~/.config/yunshu/memory.json` |
| `memory.write` | `NewTool[MemoryWriteInput]` | 写长期记忆value 支持任意 JSON 类型 |
#### 实际子 Agent
| Agent | 文件 | 状态 |
|-------|------|------|
| dialog-agent.md | `agents/dialog-agent.md` | 主持者type:main含多步骤编排指令 |
| weather-sub.md | `agents/weather-sub.md` | 天气子 Agenttype:subMarkdown 排版 + 生活建议 |
### ✨ 计划外新增
#### 泛型 + 反射工具注册(`toolschema.go`
受 Charmbracelet/Fantasy 启发,引入 `NewTool[T any]()` 泛型构造函数:
- 输入结构体 `json`/`description`/`enum` tags → 自动反射生成 JSON Schema
- 消除 ~120 行手写 Schema 模板代码(旧的 `ToolParameter`/`ToolProperty` 类型已删除)
- handler 内参数为类型安全的结构体字段,无需 `args["x"].(string)` 类型断言
- 支持嵌套结构体、slice、map、基础类型、interface{} 类型
#### 多步骤编排runtime 改造)
移除 `capturedOutput` 覆写机制,子 Agent 结果作为普通工具响应留在对话上下文中:
- 主 Agent 可以连续多次调 `task()`weather → train → hotel
- 每次返回后 LLM 继续推理,决定下一步
- 信息收集完毕再综合回答,不再被 `capturedOutput` 截断
- 单步骤查询如纯天气行为不变LLM 按 prompt 指令直接输出子 Agent 结果
#### 其他变更
- `http-get.headers` 从 JSON 字符串改为 `map[string]string`LLM 直接传对象
- `memory.write.value``string` 改为 `interface{}`,直接存储任意 JSON 值
- `types.go`:删除旧的 `ToolParameter`/`ToolProperty` 结构体,新增 `Schema(map[string]any)` 类型
- `catalog.go``buildToolList` 适配新版 Schema
- `agents/dialog-agent.md`:加入多步骤编排指令 + 数据传递说明
### 文档更新
- `docs/architecture.md`:更新工具列表、核心流程、当前状态
- `docs/AGENTS.md`:工具注册规范更新为 `NewTool[T]` 方式
- `docs/会议室架构计划书.md`:添加实现状态标记、多步骤编排章节、泛型注册章节
---
### 架构规划:会议室模式
完成从单 Agent 到"会议室架构"的完整设计,核心变更:
**新增角色体系**
- `type: main` — 主持者(对话 Agent唯一用户入口
- `type: sub` — 发言人(领域子 Agent`task` 调才说话
**新增工具**(待实现):
- `task` — 调度子 Agent + 缓存管理
- `memory.read` / `memory.write` — 长期记忆读写
**新增 Cache 机制**
- 子 Agent Frontmatter 声明 `cache.ttl` + `cache.keys`
- `task` 工具机械化拼 key、查/写缓存
- 一个 Agent 一个缓存 JSON 文件,子 Agent 无感知
**设计文档**
- `docs/会议室架构计划书.md` — 完整架构方案
- `docs/architecture.md` — 更新后续演进章节
- `docs/AGENTS.md` — 更新 Agent 定义规范type, cache 字段)
- `docs/taolun.md` — 追加 2026-05-11 讨论历史
**MSN 天气接口更新**
- 新增 `hourlyforecast` 端点文档
- 标记 `weathertrends` 为已失效
- 更新 `skills/msn-weather-api/SKILL.md``agents/weather-agent.md`
### 技术细节
- Frontmatter 新增 `type` 字段main/sub
- Frontmatter 新增 `cache` 字段(`{ttl: int, keys: [string]}`
- 用户配置目录 `~/.config/yunshu/` 下可选覆盖 agents/
- 详见 `docs/会议室架构计划书.md`
## [1.1.0] - 2026-05-09
### 发布摘要
第二版发布。核心变化Markdown 渲染器从"一行流"重构为 AST 架构,新增终端视觉系统(标题符号 + Monet 配色Go 版本升级至 1.25,项目结构从 `src/` 扁平目录重组为根目录 + `pkg/` 子包架构。
---
### 新增pkg/mdprint — Markdown → ANSI 渲染引擎
从头编写的 AST 架构渲染引擎,替代原来的"一行流"字符串匹配逻辑:
**块级解析**`parse.go`):有限状态机逐行扫描,识别 7 种块级元素
| 类型 | 语法 | 解析策略 |
|------|------|---------|
| Heading | `#` ~ `######` | 前缀匹配,记录级别 |
| CodeBlock | ` ``` ` fence | 状态切换,支持语言标识 |
| Blockquote | `>` 前缀 | 前缀剥离,递归解析 |
| List | `-` / `*` / `1.` | 前缀匹配,自动编号检测 |
| Table | `|` 分隔 | 行首检测,首行为表头 |
| ThematicBreak | `---` 独占一行 | 精确匹配 |
| Paragraph | 默认兜底 | 连续非空文本块合并 |
**行内解析**`inline.go`):递归下降扫描,支持 4 种行内元素嵌套
| 类型 | 语法 | 特性 |
|------|------|------|
| Bold | `**text**` | 可嵌套 Italic / Code / Link |
| Italic | `*text*` | 可嵌套 Bold / Code / Link |
| Code | `` `text` `` | 原始文本(内部不解析) |
| Link | `[text](url)` | text 可嵌套 Bold / Italic |
**ANSI 渲染**`render.go`type switch 按节点类型分发,标题按级别分配颜色和符号
**测试覆盖**19 个单元测试,覆盖所有块级/行内类型的正常、边界和嵌套场景
---
### 新增:标题视觉系统
用符号 `` / `` 替代 Markdown 原生 `#` 前缀,视觉效果更接近"标题"而非"标记"
| 级别 | 符号 | 字体 | 色彩Monet 睡莲) | 色值 |
|------|------|------|-------------------|------|
| H1 | `` | 加粗 | 雾蓝灰 | `#6B8E9B` |
| H2 | `` | 加粗 | 鼠尾草绿 | `#89A894` |
| H3 | `` | 加粗 | 薄荷青 | `#A6C0B5` |
| H4 | `` | 加粗 | 淡紫粉 | `#C3B1BD` |
| H5 | `` | 加粗 | 暖灰绿 | `#7B8E8A` |
| H6 | 无 | 加粗(Dim) | 浅灰 | 继承 |
排版规则所有标题前插空行H1 前后各插空行,`---` 横线前后空行,响应与输入之间空行,输出末尾空行。
---
### 新增pkg/style — 终端颜色样式库
在原有 8 色 ANSI 基础上新增 24-bit 真彩色支持:
- `Fg(color)` / `Bg(color)`:保留原有 8 色 API
- `FgHex("#RRGGBB")` / `BgHex("#RRGGBB")`:新增真彩色,输出 `\033[38;2;R;G;Bm` 格式
- 所有 `.Render(text)` 使用统一占位板 `.codes []string`,颜色和样式可组合
- `NO_COLOR` / `TERM=dumb` 环境变量自动禁用颜色
---
### 新增pkg/termui — 终端输入组件
提供三个交互式输入函数,统一使用 `bufio.NewReader(os.Stdin)` + `ensureLineMode()` 解决 Windows 控制台输入问题:
| 函数 | 用途 | 特性 |
|------|------|------|
| `ReadLine()` | 基础行输入 | 去除 `\r\n` |
| `TextInput()` | 文本输入 | 支持默认值、必填校验、自定义验证器 |
| `PasswordInput()` | 密码输入 | 输入后隐藏回显,用 `*` 遮盖 |
| `Confirm()` | 确认提示 | 支持 Y/n / y/N 默认值 |
---
### 修复
1. **行内解析器未闭合分隔符死循环**:扫描 `*` / `` ` `` 分隔符时未处理文件末尾无闭合标记的情况,改为找到匹配闭合或到达末尾时终止
2. **代码 fence 不识语言标识**` ```go `` ```json ` 等带语言的 fence 被当作文本行,改为行首 ` ``` ` 后允许非空白后缀
3. **Windows 控制台输入模式冲突**`bufio.Scanner` 在 Windows 控制台因 `ENABLE_WINDOW_INPUT` 标志导致 `Scan()` 永久阻塞。改为每轮输入前调用 `GetConsoleMode` + `SetConsoleMode` 确保模式为 `ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT`,并使用 `bufio.NewReader.ReadString('\n')` 替代 Scanner
4. **尝试自实现 Tab 补全未成功**后放弃raw mode + `ReadConsoleInputW` 方案遇到光标位置计算不一致问题,最终回退到简单 `ReadLine()`。后续如需补全功能引入第三方库
---
### 变更
- **项目结构重组**`src/` 目录删除,全部 `.go` 文件移至根目录;新增 `pkg/` 子包,按职责分离为 `mdprint/``style/``termui/`
- **模块名修改**`yunshu``hub.gaomia.site/titor/YunShu`
- **Go 版本升级**`1.21``1.25.0`
- **`Completer` 类型和 `ReadLineWithCompletion` 函数移除**`completer.go` 清空,相关测试删除
- **`.gitignore` 修复**`yunshu.exe``yunshu.exe*`(避免调试版本遗漏)
---
### 技术栈
- **语言**Go 1.25.0
- **依赖**:仅 `gopkg.in/yaml.v3`
- **默认 LLM**:豆包(火山引擎)`doubao-seed-2-0-pro-260215`
- **数据源**MSN 天气非公开 API`assets.msn.cn`
- **运行平台**Windows 10+(基于 kernel32 控制台 APIANSI VT 处理)
- **输入编码**UTF-8通过 `SetConsoleOutputCP(65001)` 设置控制台代码页)
## [1.0.0] - 2026-05-08
### 发布说明
第一个稳定发布版本。云枢·Agent 作为可独立运行的 Agent 系统,支持通过 LLM + 工具注册表 + 外挂 Agent 定义实现自然语言驱动的天气查询。
### 功能
- 三层分离架构Agent Skill行为↔ 普通 Skill知识↔ Tool确定性执行
- 外挂 Agent 定义:`.md` 文件即 AgentYAML frontmatter + Markdown body
- 4 个内置工具:`http-get``skill``read-file``geocode`
- Session 会话管理:`~/.config/yunshu/session.json` 持久化对话历史
- 交互模式 + 单次查询双模式运行
- `onboard` 交互式初始化向导
- 双路径搜索:项目目录优先,`~/.config/yunshu/` 后备
- 旧配置自动迁移:`~/.config/weather-cli/``~/.config/yunshu/`
- LLM 配置:支持配置文件 + 环境变量双重配置,兼容 OpenAI Chat Completion API
### 技术栈
- 语言Go 1.21
- 依赖:仅 `gopkg.in/yaml.v3`
- 默认 LLM豆包火山引擎`doubao-seed-2-0-pro-260215`
- 数据源MSN 天气非公开 API`assets.msn.cn`
## [1.0.0-rc.1] - 2026-05-07
### 重大变更
- **项目更名**weather-cia → **云枢·Agent**(英文名 YunShu / yunshu
- **配置目录迁移**`~/.config/weather-cli/``~/.config/yunshu/`(自动迁移)
- 二进制名称改为 `yunshu`
## [0.3.0] - 2026-05-07
### 新增
- `geocode` 工具:通过 wttr.in 查询城市坐标,支持中文和英文城市名
- `skills/geocoding/SKILL.md`:地理编码验证规则(同名城市检测、国家核对)
- 架构分离agent skill 只放行为,普通 skill 只放知识tool 负责确定性执行
### 变更
- `agents/weather-agent.md` 精简为纯行为定义(去掉所有 MSN API 内联细节,改为按需加载 skill
- 城市定位方式:从静态 cities.json 查表 → 调用 `geocode` 工具实时查询
- `agents/weather-agent.md` tools 新增 `geocode`
- session 文件从项目目录移至 `~/.config/weather-cli/session.json`
## [0.2.0] - 2026-05-07
### 新增
- `onboard` 子命令:交互式初始化向导,引导用户配置 LLM 连接信息
- 全局配置文件 `~/.config/weather-cli/config.yaml`,存储 LLM host/model/key
- 双路径搜索机制:项目目录优先,`~/.config/weather-cli/` 后备
- 首次运行检测:未配置时提示用户运行 `weather-cia onboard`
### 变更
- 项目重命名为 `weather-cia`
- 配置加载改为:配置文件 → 环境变量(环境变量优先级更高)
- Agent/skill 搜索路径扩展:项目目录 → 全局配置目录
- `onboard` 自动复制默认 agents/skills/data 到全局配置目录
## [0.1.0] - 2026-05-07
### 新增
- 项目初始化,基于 Go 实现的轻量级 agent 框架
- 核心架构:.md 文件定义 agent 行为,代码只负责加载和执行
- 工具系统声明式注册http-get, skill, read-file
- Session 会话管理session.json 记录对话历史,支持上下文追问
- 天气情报官 agentweather-agent.md通过 MSN 天气 API 查询实时天气和预报
- MSN 天气 API Skillmsn-weather-api/SKILL.mdAPI 知识按需加载
- 内置 42 个中国城市经纬度数据库data/cities.json
- 支持单次查询和交互模式两种运行方式
- 默认集成豆包火山引擎LLM通过环境变量可切换
### 技术细节
- 语言Go 1.21
- 依赖:仅 gopkg.in/yaml.v3用于解析 frontmatter
- API 兼容 OpenAI Chat Completion 格式
- 环境变量:`OPENAI_API_KEY`(必填)、`LLM_ENDPOINT``LLM_MODEL`