2026-05-08 10:12:31 +08:00
|
|
|
|
# 编码规范
|
|
|
|
|
|
|
|
|
|
|
|
## 通用规范
|
|
|
|
|
|
|
2026-05-09 03:55:56 +08:00
|
|
|
|
- 全程使用中文书写注释、文档、沟通以及内部思考过程
|
2026-05-08 10:12:31 +08:00
|
|
|
|
- 所有代码必须包含详细的中文注释,说明函数功能、参数含义、关键逻辑
|
|
|
|
|
|
- Markdown 文件使用 `#` 标题层级,保持结构清晰
|
|
|
|
|
|
- 变量命名采用驼峰式,类型和函数首字母大写导出
|
|
|
|
|
|
- 同一个问题连续工作 3 次没有结论,立即退出并询问用户接下来怎么做
|
|
|
|
|
|
|
|
|
|
|
|
## Go 代码规范
|
|
|
|
|
|
|
|
|
|
|
|
- 使用 `package main` 扁平结构(MVP 阶段),后续可拆分子包
|
|
|
|
|
|
- 错误处理:所有可能失败的操作必须检查 error
|
|
|
|
|
|
- 错误信息使用中文描述
|
|
|
|
|
|
- 导出函数(首字母大写)供包外调用,非导出函数(首字母小写)为内部实现
|
|
|
|
|
|
- HTTP 客户端设置超时(默认 15s),避免资源泄漏
|
|
|
|
|
|
- JSON 序列化/反序列化使用 `encoding/json` 标准库
|
2026-05-09 03:55:56 +08:00
|
|
|
|
- 终端颜色输出优先使用 `pkg/style` 包:
|
|
|
|
|
|
- 8 色用 `Fg(ColorXxx)` / `Bg(ColorXxx)`
|
|
|
|
|
|
- 真彩色用 `FgHex("#RRGGBB")` / `BgHex("#RRGGBB")`
|
|
|
|
|
|
- 所有通配 `.Render(text)` 生成 ANSI 转义序列
|
2026-05-08 10:12:31 +08:00
|
|
|
|
|
|
|
|
|
|
## Agent 定义规范(.md 文件)
|
|
|
|
|
|
|
|
|
|
|
|
- 必须包含 YAML frontmatter(以 `---` 包裹)
|
2026-05-11 08:32:30 +08:00
|
|
|
|
- frontmatter 必需字段:`name`, `description`, `type`, `tools`
|
|
|
|
|
|
- `type` 可选值:`main`(主持者/对话 Agent,唯一入口)、`sub`(领域专家,被 task 调)
|
|
|
|
|
|
- `cache` 可选字段:`ttl`(过期秒数)、`keys`(从 args 中提取的缓存 key 字段列表)
|
2026-05-08 10:12:31 +08:00
|
|
|
|
- tools 为数组,声明 agent 需要的工具名(在 tool.go 中注册)
|
|
|
|
|
|
- body 为 system prompt,**只定义行为逻辑**(角色、工作流程、输出规范)
|
|
|
|
|
|
- **关键技术细节(URL、apiKey、请求头、JSON 路径等)不要 inline 在 agent skill 中**,改为:
|
|
|
|
|
|
- 放到 `skills/*/SKILL.md` 中,由 agent 调用 `skill("name")` 按需加载
|
|
|
|
|
|
- 或注册为 tool(确定性操作),由 agent 声明 tools 即可调用
|
2026-05-11 08:32:30 +08:00
|
|
|
|
- session 文件存在 `~/.config/yunshu/session.json`,不污染项目目录
|
2026-05-08 10:12:31 +08:00
|
|
|
|
|
2026-05-11 08:32:30 +08:00
|
|
|
|
### 主持者示例(type: main)
|
2026-05-08 10:12:31 +08:00
|
|
|
|
|
|
|
|
|
|
```markdown
|
|
|
|
|
|
---
|
2026-05-11 08:32:30 +08:00
|
|
|
|
name: dialog
|
|
|
|
|
|
type: main
|
|
|
|
|
|
description: 个人助理,负责闲聊和调度
|
|
|
|
|
|
tools:
|
|
|
|
|
|
- task
|
|
|
|
|
|
- memory.read
|
|
|
|
|
|
- memory.write
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 对话助理
|
|
|
|
|
|
你是用户的私人助理...
|
|
|
|
|
|
|
|
|
|
|
|
你可以调度以下子 Agent:
|
|
|
|
|
|
- weather: 天气查询
|
|
|
|
|
|
- earthquake: 地震信息
|
|
|
|
|
|
|
|
|
|
|
|
用 task("agent_name", {args}) 调度。
|
|
|
|
|
|
不自己回答领域问题。
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 子 Agent 示例(type: sub)
|
|
|
|
|
|
|
|
|
|
|
|
```markdown
|
|
|
|
|
|
---
|
|
|
|
|
|
name: weather
|
|
|
|
|
|
type: sub
|
|
|
|
|
|
description: 天气查询专家
|
|
|
|
|
|
cache:
|
|
|
|
|
|
ttl: 7200
|
|
|
|
|
|
keys: ["city", "forecast_type"]
|
2026-05-08 10:12:31 +08:00
|
|
|
|
tools:
|
|
|
|
|
|
- http-get
|
|
|
|
|
|
- geocode
|
|
|
|
|
|
- skill
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-05-11 08:32:30 +08:00
|
|
|
|
# 天气专家
|
|
|
|
|
|
你是天气领域的专家。被调时才回答。
|
|
|
|
|
|
|
|
|
|
|
|
被调时你会收到:
|
|
|
|
|
|
- args: 查询参数
|
2026-05-16 17:21:29 +08:00
|
|
|
|
- cache_data: 上次缓存的原始数据(有则传,无则 null)
|
2026-05-11 08:32:30 +08:00
|
|
|
|
|
2026-05-16 17:21:29 +08:00
|
|
|
|
有 cache_data 且未过期 → 直接回答,不使用 http-get。
|
2026-05-11 08:32:30 +08:00
|
|
|
|
无 cache_data → 调 http-get 获取新数据。
|
2026-05-08 10:12:31 +08:00
|
|
|
|
|
2026-05-16 17:21:29 +08:00
|
|
|
|
返回格式(两段式):
|
|
|
|
|
|
---RESULT---
|
|
|
|
|
|
{结构化 JSON 数据,如 {"temp": 25, "condition": "晴"}}
|
|
|
|
|
|
---TEXT---
|
|
|
|
|
|
你想要对用户说的文本
|
2026-05-08 10:12:31 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-16 17:21:29 +08:00
|
|
|
|
**返回协议说明**:
|
|
|
|
|
|
- `---RESULT---`:原始数据,进缓存,不进 dialog 上下文
|
|
|
|
|
|
- `---TEXT---`:陈述文本,进 dialog 上下文,由 dialog 用自己的风格说出去
|
|
|
|
|
|
- 如果本次没有新数据(比如 cache 命中后直接复述),可不带 `---RESULT---`
|
|
|
|
|
|
|
2026-05-08 10:12:31 +08:00
|
|
|
|
## Session 规范
|
|
|
|
|
|
|
2026-05-16 17:21:29 +08:00
|
|
|
|
- 文件路径:`~/.config/yunshu/session/session.json`
|
2026-05-08 10:12:31 +08:00
|
|
|
|
- 格式:JSON 数组,元素为 Message 对象(兼容 OpenAI Chat Completion messages 格式)
|
|
|
|
|
|
- 角色类型:`system`, `user`, `assistant`, `tool`
|
|
|
|
|
|
- 启动时清空,每轮对话追加
|
|
|
|
|
|
- 消息顺序即对话顺序
|
|
|
|
|
|
- 放在用户配置目录而非项目目录,确保不同目录下运行时上下文连贯
|
|
|
|
|
|
|
|
|
|
|
|
## 工具注册规范
|
|
|
|
|
|
|
2026-05-16 17:21:29 +08:00
|
|
|
|
- 工具在 `tool.go` 的 `init()` 中通过 `RegisterTool()` + `NewTool[T]()` 注册
|
|
|
|
|
|
- `NewTool[T any](name, desc string, fn func(T) (string, error))` 泛型函数自动处理一切:
|
|
|
|
|
|
- 用 `toolschema.go`(`structToSchema` + `typeToSchema`)反射推导 JSON Schema
|
|
|
|
|
|
- JSON 往返桥接:`map[string]any` ↔ 类型安全的输入结构体
|
|
|
|
|
|
- handler 内直接访问结构体字段,无需 `args["x"].(string)` 类型断言
|
|
|
|
|
|
- 注册路径:`tool.go:init()` → `NewTool[T]()` → `RegisterTool()`
|
2026-05-08 10:12:31 +08:00
|
|
|
|
- 工具名与 `.md` 文件中声明的 tools 列表对应
|
2026-05-16 17:21:29 +08:00
|
|
|
|
- 新增工具三步:定义输入结构体(struct tags: json, description, enum)→ 写 handler → 调用 `NewTool[T]()`
|
|
|
|
|
|
|
|
|
|
|
|
## 日志系统
|
|
|
|
|
|
|
|
|
|
|
|
日志系统分两层:**控制台输出**(charmbracelet/log → stderr)和**文件持久化**(YAML 序列 → `log.yml`)。
|
|
|
|
|
|
|
|
|
|
|
|
### 日志写入
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
warnLog(msg string, keyvals ...any) // 写 log.yml + 可选 stderr
|
|
|
|
|
|
errorLog(msg string, keyvals ...any) // 同上
|
|
|
|
|
|
infoLog(msg string, keyvals ...any) // 同上
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- stderr 输出受 `logToStderr` 全局开关控制(交互模式默认关,`/log on` 开启)
|
|
|
|
|
|
- `log.yml` 始终写入,YAML 序列格式,每次读→追加→重写
|
|
|
|
|
|
|
|
|
|
|
|
### 日志查看
|
|
|
|
|
|
|
|
|
|
|
|
`yunshu log` 子命令:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
yunshu log → 全量显示(时间倒序)
|
|
|
|
|
|
yunshu log --top N → 只看最后 N 条
|
|
|
|
|
|
yunshu log --level warn → 过滤级别
|
|
|
|
|
|
yunshu log --clear → 清空
|
|
|
|
|
|
yunshu log --watch → 监听模式(2s 轮询)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 日志点
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
[INFO] task(weather) 开始
|
|
|
|
|
|
[INFO] 子 Agent 开始 agent=weather
|
|
|
|
|
|
[INFO] LLM 调用完成 tokens=1420 duration=3.2s
|
|
|
|
|
|
[WARN] 解析 session.json 失败 err=...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
关键原则:
|
|
|
|
|
|
- 成功日志用 `infoLog`(task 开始/完成、LLM 调用完成、会话清空)
|
|
|
|
|
|
- 异常日志用 `warnLog`(解析失败、文件不存在的异常错误)
|
|
|
|
|
|
- 错误日志用 `errorLog`(工具执行失败、子 Agent 未找到)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
2026-05-08 10:12:31 +08:00
|
|
|
|
|
|
|
|
|
|
## 环境变量
|
|
|
|
|
|
|
|
|
|
|
|
| 变量名 | 必需 | 说明 |
|
|
|
|
|
|
|--------|------|------|
|
|
|
|
|
|
| `LLM_API_KEY` | 否* | API Key,覆盖配置文件 |
|
|
|
|
|
|
| `LLM_ENDPOINT` | 否 | API 端点,覆盖配置文件 |
|
|
|
|
|
|
| `LLM_MODEL` | 否 | 模型名,覆盖配置文件 |
|
|
|
|
|
|
| `OPENAI_API_KEY` | 否 | 兼容旧名,当 `LLM_API_KEY` 未设置时生效 |
|
|
|
|
|
|
|
2026-05-16 17:21:29 +08:00
|
|
|
|
> *注:可在 `~/.config/yunshu/config.yml` 中配置,无需环境变量。
|
2026-05-08 10:12:31 +08:00
|
|
|
|
> 首次使用请运行 `yunshu onboard` 交互式初始化。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 【认知修正】
|
|
|
|
|
|
|
|
|
|
|
|
> 本字段存放开发过程中验证后的知识点、踩坑记录。以陈述句形式记录。
|
|
|
|
|
|
|
|
|
|
|
|
### 2026-05-07
|
|
|
|
|
|
|
|
|
|
|
|
1. **MSN 天气 API 属于非公开内部接口**,apiKey 固定为 `j5i4gDqHL6nGYwx5wi5kRhXjtf2c5qgFX9fzfk0TOo`,修改任意字符即 401。必须携带 `User-Agent` 和 `Referer` 请求头,否则返回 401。响应数据在 `value[0].responses[0].weather[0]` 路径下。
|
|
|
|
|
|
|
|
|
|
|
|
2. **Go 的 `syscall` 包是标准库,无需额外依赖**。在 Windows 上可通过 `kernel32.SetConsoleOutputCP(65001)` 设置控制台 UTF-8 编码,但 PowerShell 5.1 有独立的 `[Console]::OutputEncoding` 覆盖此设置,需要额外 `[Console]::OutputEncoding = [Text.Encoding]::UTF8`。
|
|
|
|
|
|
|
|
|
|
|
|
3. **豆包(火山引擎)API 兼容 OpenAI Chat Completion 格式**,包括 function calling(tool_calls)。修改 `endpoint` 和 `model` 即可切换,无需改动代码逻辑。实测 `doubao-seed-2-0-pro-260215` 支持工具调用正常。
|
|
|
|
|
|
|
|
|
|
|
|
4. **非流式调用更简单可靠**。对于 CLI 工具,等待完整响应再输出比流式逐 token 输出实现更简单,且用户能一次获取完整信息。
|
|
|
|
|
|
|
|
|
|
|
|
5. **Session 文件的关键设计**:session 存储的是完整的对话消息列表(不含 system prompt),格式与 OpenAI Chat Completion API 的 messages 数组一致。这意味着 runtime 不需要做任何格式转换,读 session → 直接 POST 给 LLM → 拿到回复 → 追加到 session。
|
|
|
|
|
|
|
|
|
|
|
|
6. **Go 的 `gopkg.in/yaml.v3` 依赖可能遇到 GOSUMDB 问题**。在中国网络环境下,需要设置 `GONOSUMCHECK='*'` 和 `GONOSUMDB='*'` 环境变量来绕过 checksum 数据库验证。
|
|
|
|
|
|
|
|
|
|
|
|
7. **工具定义要提供清晰的 JSON Schema 参数描述**。LLM 通过参数描述来理解如何调用工具。描述越清晰,LLM 生成正确参数的概率越高。`http-get` 工具的 `headers` 参数设计为 JSON 字符串格式,比结构化对象更灵活。
|
|
|
|
|
|
|
|
|
|
|
|
8. **Go 中处理 OpenAI 响应的 Content 字段要使用指针类型**。当 LLM 返回 tool_calls 时,content 字段为 null(JSON 中的 null),而非空字符串。使用 `*string` 才能区分"内容为空"和"无内容"两种情况。
|
|
|
|
|
|
|
|
|
|
|
|
9. **配置文件放在 `~/.config/yunshu/config.yaml` 而非 .env/.secret**。YAML 格式与 agent 定义风格一致,统一管理。API Key 用 `0600` 权限保护。优先顺序:环境变量 > 配置文件 > 默认值。`onboard` 子命令提供交互式初始化体验。
|
|
|
|
|
|
|
|
|
|
|
|
10. **双路径搜索机制**:项目目录优先,`~/.config/yunshu/` 后备。这使得开发时用项目本地文件,部署后自动切换到全局配置。`SearchFile()` 和 `LoadAgent()/LoadSkill()` 都遵循此规则。
|
|
|
|
|
|
|
|
|
|
|
|
11. **用户配置目录固定为 `~/.config/yunshu/`**,所有系统统一。存放 config.yaml、session.json、以及用户自定义的 agents/skills/data。不能改到其他路径。
|
|
|
|
|
|
|
|
|
|
|
|
12. **Agent skill、普通 skill、tool 必须严格分离,不能混淆**。Agent skill(`agents/*.md`)只放行为逻辑(角色、工作流程、输出风格),不 inline 任何技术细节。技术细节(URL、apiKey、请求头、JSON 解析路径)放在 `skills/*/SKILL.md` 作为纯知识,由 LLM 按需调用 `skill("name")` 加载。确定性操作(如 geocode)注册为 tool,保证 100% 可靠执行。这解决了 picoclaw 单 agent 架构下 skill 污染上下文的问题。
|
|
|
|
|
|
|
|
|
|
|
|
13. **wttr.in `?format=j1` 返回的 JSON 包含地理编码信息**,`nearest_area[0]` 中有 `latitude`、`longitude`、`areaName`、`country`、`population` 字段。可作为免费的地理编码服务使用,无需 API Key。
|
|
|
|
|
|
|
|
|
|
|
|
14. **geocode 工具用 Go 代码实现比让 LLM 自己调 http-get 解析 JSON 更可靠**。LLM 在构造 URL 和解析嵌套 JSON 时容易出错(尤其是中文编码问题)。注册为 tool 后,LLM 只需提供城市名参数,Go 代码处理所有细节。
|
|
|
|
|
|
|
|
|
|
|
|
15. **项目正式命名为云枢·Agent(YunShu / yunshu)**,配置目录从 `~/.config/weather-cli/` 迁移到 `~/.config/yunshu/`。旧目录在首次运行时会自动迁移并删除。二进制名称改为 `yunshu`。如果迁移失败,用户可手动复制旧目录内容后重新运行。
|
2026-05-09 03:55:56 +08:00
|
|
|
|
|
|
|
|
|
|
### 2026-05-09
|
|
|
|
|
|
|
|
|
|
|
|
1. **Windows raw mode + bufio.Reader 冲突**:在禁用 `ENABLE_LINE_INPUT` 的 raw mode 下,`bufio.NewReader(os.Stdin).ReadRune()` 会因内部预读缓冲(默认 4KB)导致读取阻塞。后续尝试用 `ReadConsoleInputW` + 手动回显也因光标位置计算不一致而放弃。**最终决定放弃自实现 Tab 补全**,后续如需补全功能,引入第三方库(如 `go-prompt` / `readline`)。
|
|
|
|
|
|
|
|
|
|
|
|
2. **ANSI 光标移动需要启用输出 VT 处理**:`\033[A`(光标上移)/ `\033[J`(清屏)等输出序列需要输出句柄设置 `ENABLE_VIRTUAL_TERMINAL_PROCESSING`(0x0004)才能生效。只设置输入句柄的 mode 不够,输入输出句柄是两个独立的控制台句柄。
|
|
|
|
|
|
|
|
|
|
|
|
3. **ReadLineWithCompletion / Completer 类型已移除**。`completer.go` 清空,`main.go` 回到 `termui.ReadLine()`。`cmdCompleter`、`commonPrefix`、相关测试一并移除。`input.go` 中 `ReadConsoleInputW` 相关的 `keyEventRecord`/`inputRecord` 结构和 proc 也一并清理。
|
2026-05-11 08:32:30 +08:00
|
|
|
|
|
|
|
|
|
|
### 2026-05-11
|
|
|
|
|
|
|
|
|
|
|
|
1. **MSN 天气存在 `hourlyforecast` 端点**:`https://assets.msn.cn/service/weather/hourlyforecast`,返回未来 10 天逐小时预报数据,参数和 dailyforecast 一致。之前文档遗漏了该端点。`weathertrends` 端点已失效(500 Internal Server Error)。
|
|
|
|
|
|
|
|
|
|
|
|
2. **MSN 的 `assets.msn.cn` 国内城市接口正常**:用经纬度查询 `assets.msn.cn/service/weather/current` 对国内城市返回正确数据。之前 Agent 误报"返回也门数据"是因为用了 `api.msn.cn` 的城市名接口(该接口本就有文档标注的问题:城市名匹配不可靠)。**用 `assets.msn.cn` + 经纬度即可正常获取国内城市数据,不需要切换到 wttr.in。**
|
|
|
|
|
|
|
|
|
|
|
|
3. **会议室架构决策**:确定从单 Agent 升级为 1 主持(dialog, type:main)+ N 领域专家(type:sub)+ 共享黑板(memory)的架构。主持者保持极薄(只有人格 + 调度规则 + `task` + `memory` 工具),子 Agent 只做领域工作,用完即毁。详见 `docs/会议室架构计划书.md`。
|
|
|
|
|
|
|
|
|
|
|
|
4. **Cache 设计原则**:Frontmatter 声明 `cache.keys` 和 `cache.ttl`,`task` 工具机械化从 args 取值拼 key、查/写缓存。子 Agent 不感知缓存存在,只需在回答末尾可选带 `---CACHE---` + JSON 供 task 存储。一个 Agent 一个缓存 JSON 文件,MD5 hash 做 key。
|
|
|
|
|
|
|
|
|
|
|
|
5. **记忆系统规则**:共享黑板模式,所有 Agent 可读,仅 memory Agent 可写。dialog-agent 是最小写入者(只写 `dialog_context`),memory Agent 负责从对话中提取用户画像写入长期记忆。
|
2026-05-16 17:21:29 +08:00
|
|
|
|
|
|
|
|
|
|
6. **子 Agent 返回协议 `---RESULT---` / `---TEXT---` 双段设计**:子 Agent 回答分两段——RESULT 是原始结构化数据(进缓存,不进 dialog 上下文),TEXT 是陈述文本(进 dialog 上下文,由 dialog 用自己的风格输出)。这样 dialog 不会产生"复述感",子 Agent 的数据也保持干净。
|
|
|
|
|
|
|
|
|
|
|
|
7. **两次 LLM 调用架构**:用户提问 → dialog 的 LLM 判断 → `task` 工具调子 Agent 的 LLM(第一次)→ 子 Agent 返回 TEXT → dialog 的 LLM 用自己的风格说出去(第二次)。各司其职,职责单一。
|
|
|
|
|
|
|
|
|
|
|
|
8. **`task` 工具内部调用 `RunSubAgent()` 实现子 Agent 执行**:`RunSubAgent` 不读写全局 session.json,子 Agent 的 tool_calls 用完即毁,不污染主上下文。返回后由 `task` 工具解析 `---RESULT---` / `---TEXT---`。
|
|
|
|
|
|
|
|
|
|
|
|
### 2026-05-16
|
|
|
|
|
|
|
|
|
|
|
|
1. **charmbracelet/log v2(`charm.land/log/v2`)用作结构化日志**。输出到 stderr 时自动带时间戳和级别前缀,彩色渲染。v2 模块路径改为 `charm.land/log/v2`(非 `github.com/charmbracelet/log/v2`)。`log.NewWithOptions(w, opt)` 创建实例,方法签名:`log.Warn(msg, keyvals...)`、`log.Error(msg, keyvals...)`、`log.Info(msg, keyvals...)`。
|
|
|
|
|
|
|
|
|
|
|
|
2. **log.yml 使用 YAML 序列追加策略**。每次写入先读(反序列化为 `[]logEntry`)→ 追加 → 重写(`yaml.Marshal`)。不是 JSONL,不是纯文本。适用于 CLI 工具的日志量级。`readLogs()` 返回全部条目用于 `yunshu log` 展示。
|
|
|
|
|
|
|
|
|
|
|
|
3. **`RunSubAgent` 加 `maxToolCalls=2` 安全上限**。子 Agent 的 LLM 循环中如果连续 2 轮都在调工具,第 3 轮强制返回 `---TEXT---\n(子 Agent 执行轮次超限,已终止)`。防止 prompt 设计缺陷导致子 Agent 死循环。
|
|
|
|
|
|
|
|
|
|
|
|
4. **单 Agent 查询性能优化**。dialog-agent 的 prompt 明确指示:对于只需调一个子 Agent 的查询,跳过 observation 写入和 summary 写入,直接输出子 Agent 结果。综合查询的 observation + summary 合并在同一轮 memory.write 调用。实测 note 保存从 ~90s 降至 ~10s。
|
|
|
|
|
|
|
|
|
|
|
|
5. **路径安全使用 `filepath.EvalSymlinks` + `filepath.Rel` 三段校验**:Clean → Join → EvalSymlinks → Rel。防止符号链接绕过路径限制。仅 `strings.Contains("..")` 不够,因为符号链接可以指向 ConfigDir 之外。`os.IsNotExist` 时跳过 EvalSymlinks(新文件创建场景)。
|
|
|
|
|
|
|
|
|
|
|
|
6. **`flag.ExitOnError` 用于 `yunshu log` 子命令 flag 解析**。遇到未知 flag 自动 exit(2) 并打印用法,比手动处理 flag 更简洁。与 `main.go` 的主 args 手动解析配合不冲突。
|
|
|
|
|
|
|
|
|
|
|
|
7. **流式输出使用 SSE 协议,OpenAI 兼容格式**。请求加 `"stream": true`,响应逐行 `data: {...}`,以 `data: [DONE]` 结尾。用 `bufio.Reader.ReadString('\n')` 逐行读取,不走 `io.ReadAll`。流式和非流式模式下 tool_calls 的表现不同:非流式一次返回完,流式按 index 分片到达,需要 `accumulatedToolCall` 累积合并。
|
|
|
|
|
|
|
|
|
|
|
|
8. **按 `\n\n` 段落边界缓冲后经 mdprint 渲染**。流式 token 是碎片,不能直接过 mdprint(后者需要完整 Markdown parse → AST → ANSI)。解决方案:将内容写入 blockBuf,在 `\n\n` 处切分——之前的是完整 block,调用 `mdprint.Print` 渲染到 stdout;之后的是不完整残段,留在 buffer 继续累积。流结束时 `mdprint.Print(blockBuf)` 刷最后一段。实测 weather 场景每个 section(标题/表格/分隔线)依次弹出,延迟 1-3s,渲染效果与非流式一致。
|
|
|
|
|
|
|
|
|
|
|
|
9. **流式模式下 model 可能先吐文本再吐 tool_call**。非流式 API 的 model 要么返回 content 要么返回 tool_calls,不会同时出现。但流式模式下,model 可能先输出一段"思考"文字,然后才决定调工具。这段文字已经显示给用户,之后工具返回又有第二轮 LLM 响应,就造成了"用户看到两段回复"的问题。**修复方式:prompt 层面要求 model"先调工具,不要先说话"**,在 dialog-agent.md 中新增"流式输出原则"章节。
|