- 模块名重命名 yunshu -> hub.gaomia.site/titor/YunShu - Go 版本升级 1.21 -> 1.25 - src/ 目录删除,所有文件移至根目录 - 新增 pkg/mdprint/: Markdown AST 解析+ANSI 渲染 - 新增 pkg/style/: 终端颜色样式(8色 ANSI + 24位真彩色) - 新增 pkg/termui/: 终端输入组件(交互式输入/密码/确认) - 更新文档:AGENTS.md、architecture.md、changelog.md、taolun.md - gitignore 通配符修复 yunshu.exe -> yunshu.exe*
245 lines
11 KiB
Markdown
245 lines
11 KiB
Markdown
# 讨论历史
|
||
|
||
## 2026-05-07 项目启动与架构设计
|
||
|
||
### 背景
|
||
用户有一个 `MSN天气API探索报告.md` 文档,记录了通过抓包发现的微软 MSN 天气内部 API(`assets.msn.cn`),该 API 国内访问速度快,数据完整(温度、湿度、风速、AQI、紫外线等),但属于非公开接口,无 SLA 保证。
|
||
|
||
### 目标演变
|
||
1. **最初目标**:做一个"天气情报官" agent,后期结合 TTS 和 ASR 实现语音查询播报
|
||
2. **深化**:用户想从 0 实现一个类似 opencode 主-从架构的个人 AI 助理,解决现有单 agent 框架(zeroclaw/picoclaw)的痛点:上下文污染、工具执行懒惰、skill 效果差
|
||
3. **当前范围**:先做一个最小化的 CLI 天气查询工具,验证 .md 外挂 agent 定义 + session 会话管理 + 工具注册表机制
|
||
|
||
### 架构决策
|
||
|
||
#### 为什么不用现有框架(LangChain 等)
|
||
- 核心创新是 .md 文件即 agent 定义,与任何框架都耦合不上
|
||
- 自实现核心 ~500 行,无外部依赖包袱
|
||
|
||
#### Agent 定义格式(仿 opencode)
|
||
- YAML frontmatter + Markdown body
|
||
- frontmatter 字段:name, description, type, tools, permission
|
||
- body 即 system prompt,定义角色行为
|
||
|
||
#### Session 会话机制
|
||
- `session.json` 文件存对话历史,格式兼容 OpenAI Chat Completion API messages 数组
|
||
- 每次启动清空,每轮对话追加
|
||
- 追问时 LLM 自动判断是否需要重新调 API(数据过期/不同城市)
|
||
- 通用设计,后续 master-subagent 架构也可复用
|
||
|
||
#### LLM 提供商
|
||
- 用户提供豆包(火山引擎)API:`https://ark.cn-beijing.volces.com/api/v3`
|
||
- 模型:`doubao-seed-2-0-pro-260215`
|
||
- 环境变量可配置:`LLM_ENDPOINT`, `LLM_MODEL`, `LLM_API_KEY`
|
||
|
||
### 工具系统
|
||
- 声明式注册:`tool.go` 注册工具,`.md` 文件声明即可用
|
||
- 内置工具:`http-get`, `skill`, `read-file`
|
||
- skill 工具按需加载,不预置到 system prompt
|
||
|
||
### Windows 编码问题
|
||
- PowerShell 输出编码为 GB2312,Go 输出 UTF-8 导致中文乱码
|
||
- 通过 `kernel32.SetConsoleOutputCP(65001)` 设置控制台 CP 为 UTF-8
|
||
- 在 PowerShell 中需额外执行 `[Console]::OutputEncoding = [Text.Encoding]::UTF8`
|
||
|
||
### 项目结构(最终)
|
||
```
|
||
weather/
|
||
├── main.go # CLI 入口
|
||
├── types.go # 核心类型
|
||
├── loader.go # .md 解析 + skill 加载
|
||
├── llm.go # LLM API 封装(默认豆包)
|
||
├── tool.go # 工具注册表
|
||
├── runtime.go # agent 循环 + session
|
||
├── agents/
|
||
│ └── weather-agent.md # 天气情报官定义
|
||
├── skills/
|
||
│ └── msn-weather-api/SKILL.md
|
||
├── data/
|
||
│ └── cities.json # 42 个中国城市
|
||
├── taolun.md # 本文件
|
||
├── changelog.md # 版本变更
|
||
└── agents.md # 编码规范
|
||
```
|
||
|
||
### 验证结果
|
||
- 单次查询:`.\weather-agent.exe "北京今天天气"` → 成功返回温度、湿度、AQI 等
|
||
- 交互模式:启动后连续追问 → `session.json` 记录历史,LLM 基于上下文回答"适合穿什么"
|
||
- 豆包 API 工具调用正常:自动读取 cities.json → 调 MSN API → 分析输出
|
||
|
||
---
|
||
|
||
## 2026-05-07 项目重命名与配置体系
|
||
|
||
### 变更
|
||
1. **项目重命名**:`weather-agent` → `weather-cia`(CIA = 天气情报官)
|
||
2. **配置体系**:`~/.config/weather-cli/config.yaml` 统一管理 LLM 配置
|
||
3. **初始化方式**:`weather-cia onboard` 交互式向导,替代手动写配置文件
|
||
4. **双路径搜索**:项目目录优先 + `~/.config/weather-cli/` 后备
|
||
|
||
### 关键决策
|
||
- **用 config.yaml 而非 .env/.secret**:YAML 风格与 agent 定义一致,API Key 用 0600 权限保护
|
||
- **配置优先级**:环境变量 > 配置文件 > 默认值(`init()` 中依次加载)
|
||
- **`onboard` 子命令**:交互式 TTY 输入,自动复制默认 agents/skills/data 到全局目录
|
||
- **搜索路径**:`SearchFile()` 统一管理,开发者用项目文件,用户用全局配置
|
||
|
||
### 验证
|
||
- `weather-cia onboard` 成功创建 `~/.config/weather-cli/config.yaml`
|
||
- `weather-cia "北京今天天气"` 无需环境变量,直接读取配置文件中的豆包 key 并成功返回天气数据
|
||
- 全局配置目录自动包含 agents/、skills/、data/ 的完整副本
|
||
|
||
---
|
||
|
||
## 2026-05-07 架构分离:Agent Skill vs 普通 Skill vs Tool
|
||
|
||
### 背景
|
||
参考了 picoclaw 的 weather skill 设计,对比发现:
|
||
- picoclaw 的 skill 写得很完整(含验证规则、边界情况)
|
||
- 但我们的 `weather-agent.md` 之前 inline 了大量 API 细节 → 和 picoclaw 一样污染上下文
|
||
|
||
### 决策:三层分离
|
||
|
||
| 层 | 文件位置 | 加载时机 | 上下文影响 |
|
||
|---|---------|---------|-----------|
|
||
| **Agent skill** | `agents/weather-agent.md` | 启动即加载为 system prompt | **全程** |
|
||
| **普通 skill** | `skills/*/SKILL.md` | LLM 调用 `skill("name")` 时 | **仅该轮对话** |
|
||
| **Tool** | `src/tool.go` 注册 | 预声明,LLM 调用时执行 | **仅返回结果文本** |
|
||
|
||
### 具体改造
|
||
|
||
1. **新增 `geocode` tool**(Go 代码):
|
||
- 输入城市名,调 wttr.in `?format=j1` 解析 JSON
|
||
- 返回 `{lat, lon, name, country}` 结构化数据
|
||
- 确定性执行,比 LLM 自己构造 URL 解析 JSON 更可靠
|
||
|
||
2. **新建 `skills/geocoding/SKILL.md`**:
|
||
- 纯知识:wttr.in 查询格式、JSON 解析路径
|
||
- 验证规则:同名城市检测、country 核对、population 排序
|
||
|
||
3. **精简 `agents/weather-agent.md`**:
|
||
- 去掉所有 MSN API URL、apiKey、请求头、JSON 路径等内联知识
|
||
- 改为行为描述:识别城市 → geocode → skill("msn-weather-api") → http-get → 分析
|
||
- 从 65 行缩减为 40 行,只留行为逻辑
|
||
|
||
4. **session 移至 `~/.config/weather-cli/session.json`**
|
||
|
||
### 结果
|
||
- Agent skill 保持瘦身,system prompt 不膨胀
|
||
- 知识按需加载,用完即走,不残留上下文
|
||
- Tool 执行可靠,不依赖 LLM 的 JSON 解析能力
|
||
- 三种内容互不干扰,为后续主-从架构打下基础
|
||
|
||
---
|
||
|
||
## 2026-05-07 项目更名:云枢·Agent
|
||
|
||
### 变更
|
||
1. **正式命名**:云枢·Agent(YunShu / yunshu)
|
||
- 坐看云卷云舒,静听花开花落
|
||
2. **配置目录迁移**:`~/.config/weather-cli/` → `~/.config/yunshu/`(自动迁移)
|
||
3. **二进制名称**:`yunshu`
|
||
4. **架构白皮书**:`~/Desktop/yunshu-architecture.md`
|
||
|
||
### 设计理念
|
||
"云枢"二字呼应了项目作为 AI 助理"中枢调度"的定位——云是分布式的、流转的,枢是枢纽、核心。后续主-从架构中,master 负责调度、subagent 各司其职,恰如云卷云舒。
|
||
|
||
---
|
||
|
||
## 2026-05-09 Markdown 渲染器重构 + 终端输入修复
|
||
|
||
### 背景
|
||
原本的 `pkg/mdprint/mdprint.go` 是"一行流"渲染——读取 Markdown 文本后直接正则/字符串匹配输出 ANSI。问题:heading 和后续 paragraph 无法正确分离,`#### 标题\n正文` 导致正文也被染上 heading 颜色。
|
||
|
||
### 方案:OOP 风格 AST 架构
|
||
将 `pkg/mdprint/` 拆分为多文件,各司其职:
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `mdprint.go` | `Node` 接口 + 所有块级/行内类型定义 + `Print()` 入口 |
|
||
| `parse.go` | 状态机块级解析器 `parseBlocks` |
|
||
| `inline.go` | 递归行内解析器 `parseInline` |
|
||
| `render.go` | `renderNode` type switch 渲染器 |
|
||
| `mdprint_test.go` | 单元测试(待加) |
|
||
|
||
### AST 类型设计
|
||
```
|
||
Node interface
|
||
├── Heading{Level, Content} → `#### 标题`
|
||
├── Paragraph{Content} → 普通文本块
|
||
├── CodeBlock{Lang, Body} → ```fenced```
|
||
├── Blockquote{Children} → > 引用
|
||
├── List{Ordered, Items} → - / * / 1. 列表
|
||
├── ListItem{Checked, Content} → 含 checkbox 支持
|
||
├── Table{Headers, Rows} → `| H1 | H2 |`
|
||
├── ThematicBreak{} → ---
|
||
├── Text{Text} → "纯文本"
|
||
├── Bold{Content} → **bold**
|
||
├── Italic{Content} → *italic*
|
||
├── Code{Text} → `code`
|
||
└── Link{Content, URL} → [text](url)
|
||
```
|
||
|
||
### 解析器流程
|
||
1. `Print(content)` → 按 `\n` 分割为 lines
|
||
2. `parseBlocks(lines)` → 逐行状态机,识别 heading / code fence / quote / list / table / thematic-break / paragraph → 输出 `[]Node`
|
||
3. 块内文本调 `parseInline()` → 递归扫描,识别 `**`/`*`/\`\`/`[]()` → 输出 `[]Node`
|
||
4. 遍历 `[]Node` 调 `renderNode()` → type switch 分发 → `strings.Builder` 拼接
|
||
5. `fmt.Print` 输出
|
||
|
||
### 关键修复
|
||
- Heading 和 Paragraph 是独立 AST 节点,heading 后的文本永远归 Paragraph,不会再染上 heading 颜色
|
||
- 块级解析器在 heading 行后自动切 paragraph,无需 blank line 分隔
|
||
- 代码 fence 用状态机追踪开闭
|
||
|
||
### 终端输入修复(2026-05-09)
|
||
- **Root cause**:Windows 控制台处于 character mode(`ENABLE_WINDOW_INPUT`),导致 `bufio.Scanner` 和 `ReadString` 均无法获取行输入
|
||
- **修复**:`ensureLineMode()` 在每次读之前调 `GetConsoleMode` + `SetConsoleMode` 设置为 `ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT`
|
||
- `ReadLine()` 改为 `bufio.NewReader(os.Stdin).ReadString('\n')`(每次新建 reader),避免 Scanner 的缓冲区冲突
|
||
- ANSI 样式在所有提示符和状态指示器上恢复正常显示
|
||
|
||
### 当前进度(后续完成)
|
||
- 2026-05-09 补充完成:`inline.go` + `render.go` + `mdprint_test.go`
|
||
- 行内解析器支持 `**bold**` / `*italic*` / `` `code` `` / `[link](url)`,递归嵌套
|
||
- 修复:未闭合分隔符死循环、代码 fence 不识别语言标识
|
||
|
||
---
|
||
|
||
## 2026-05-09 标题视觉系统 + 真彩色支持
|
||
|
||
### 背景
|
||
Markdown 渲染器完成 AST 解析后,需要确定标题的终端展示风格。最初方案是保留 Markdown 的 `#` 前缀,但用户反馈效果不好。
|
||
|
||
### 标题样式演进
|
||
1. **最初**:`#` 前缀 + ANSI 颜色 → 用户觉得不好看
|
||
2. **方案 A**:去掉 `#`,用 `▎` 色块 + 加粗文字 → 讨论中用户提出 `▪`/`▫` 符号更好
|
||
3. **方案 B**:H1-H3 用 `▪` + 空格,H4-H5 用 `▫` + 空格,H6 纯加粗 → 用户确认
|
||
4. **配色**:黄色/红色保留给重要信息,排除后从青/蓝/绿/品红/白中分配
|
||
5. **最终使用真彩色**:用户要求莫奈(印象派)配色,现有 8 色 ANSI 无法满足
|
||
|
||
### 最终标题配置
|
||
| 级别 | 符号 | 色值 | 色名 |
|
||
|------|------|------|------|
|
||
| H1 | `▪ ` | `#6B8E9B` | 雾蓝灰 |
|
||
| H2 | `▪ ` | `#89A894` | 鼠尾草绿 |
|
||
| H3 | `▪ ` | `#A6C0B5` | 薄荷青 |
|
||
| H4 | `▫ ` | `#C3B1BD` | 淡紫粉 |
|
||
| H5 | `▫ ` | `#7B8E8A` | 暖灰绿 |
|
||
| H6 | 无 | Dim | 浅灰 |
|
||
|
||
### 排版规则
|
||
- 所有标题前插一个空行
|
||
- 1 级标题前后各插一个空行
|
||
- `---` 横线前后各插一个空行
|
||
- 输出末尾统一加空行
|
||
- 交互模式:输入与响应之间插空行,响应与下一轮 `❯` 之间插空行
|
||
|
||
### 真彩色支持方案
|
||
- `pkg/style/style.go` 新增 `FgHex()` / `BgHex()` 方法
|
||
- 输出 `\033[38;2;R;G;Bm` 格式的 24-bit 真彩色序列
|
||
- 底层复用 `codes []string`,`Render()` 零改动
|
||
- 旧 `Fg(Color)` 8 色 API 完全兼容,不破坏已有代码
|
||
|
||
### 验证
|
||
- 19/19 单元测试通过
|
||
- 构建成功,二进制运行正常
|