Files
YunShu/onboard.go
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

155 lines
3.6 KiB
Go
Raw 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.
package main
import (
"fmt"
"os"
"path/filepath"
"hub.gaomia.site/titor/YunShu/pkg/style"
"hub.gaomia.site/titor/YunShu/pkg/termui"
)
const userMDTemplate = `# 用户画像
> 这里记录关于你的信息。通过对话自动更新,你也可以手动修改。
## 基本信息
- **称呼**
- **常驻地**
- **温度单位**:摄氏度 (C)
## 偏好
- 过敏源:
- 兴趣:
- 出行习惯:
## 备注
(自由添加,不会被自动覆盖)
`
const soulMDTemplate = `# AI 灵魂
> 这里是 AI 的灵魂设定。你可以修改下面的话,改变我的性格和说话方式。
## 人设
你是一个友好、亲切的个人助理。说话简洁直接,偶尔可以幽默。
## 注意事项
- 尊重用户的隐私
- 不主动推荐第三方服务
- 对不确定的事情要坦诚,不编造
`
func runOnboard() {
fmt.Println()
fmt.Println(style.Cyan.Render("☁ 云枢·Agent · 初始化配置"))
fmt.Println()
fmt.Println("配置说明 · 以下信息将保存在", style.Dim.Render(ConfigDir()))
fmt.Println()
host := termui.TextInput("LLM 接口地址",
termui.WithRequired(true),
termui.WithHelp("支持 OpenAI 兼容格式"),
termui.WithDefault("https://ark.cn-beijing.volces.com/api/v3/chat/completions"),
termui.WithValidator(termui.And(termui.NonEmpty, termui.IsURL)),
)
key := termui.PasswordInput("API Key",
termui.WithRequired(true),
termui.WithHelp("输入已隐藏"),
termui.WithValidator(termui.NonEmpty),
)
model := termui.TextInput("模型名称",
termui.WithHelp("默认: doubao-seed-2-0-pro-260215"),
termui.WithDefault("doubao-seed-2-0-pro-260215"),
)
fmt.Println()
ok := termui.Confirm("保存配置?", true)
if !ok {
fmt.Println(style.Yellow.Render("取消配置"))
return
}
cfg := &Config{
LLM: LLMConfig{
Host: host,
Model: model,
Key: key,
},
}
if err := SaveConfig(cfg); err != nil {
fmt.Fprintln(os.Stderr, style.Red.Render("保存配置失败: "+err.Error()))
os.Exit(1)
}
CopyDefaultDir("agents", "agents")
CopyDefaultDir("skills", "skills")
CopyDefaultDir("data", "data")
ensureUserConfig()
fmt.Println()
testOk := termui.Confirm("测试连通性?", true)
if testOk {
testLLM()
}
fmt.Println()
fmt.Println(style.Green.Render("✔ 配置完成!"))
fmt.Println(" 配置文件:", style.Dim.Render(filepath.Join(ConfigDir(), "config.yml")))
fmt.Println()
fmt.Println(" 运行示例:")
fmt.Println(" " + style.Cyan.Render("yunshu \"北京今天天气\""))
fmt.Println(" " + style.Cyan.Render("yunshu"))
}
func testLLM() {
fmt.Print(style.Dim.Render("测试中 ..."))
oldKey, oldHost, oldModel := llmKey, llmHost, llmModel
defer func() {
llmKey, llmHost, llmModel = oldKey, oldHost, oldModel
}()
cfg, err := LoadConfig()
if err != nil {
fmt.Println(style.Red.Render("\r⚠ 读取配置失败: " + err.Error()))
return
}
llmKey, llmHost, llmModel = cfg.LLM.Key, cfg.LLM.Host, cfg.LLM.Model
msg := Message{Role: RoleUser, Content: "ping"}
_, err = CallLLM([]Message{msg}, nil)
if err != nil {
fmt.Println(style.Red.Render("\r⚠ 连接失败: " + err.Error()))
return
}
fmt.Println(style.Green.Render("\r✔ 连接成功!"))
}
func ensureUserConfig() {
dir := ConfigDir()
os.MkdirAll(filepath.Join(dir, "notes"), 0755)
os.MkdirAll(filepath.Join(dir, "config"), 0755)
userPath := filepath.Join(dir, "config", "user.md")
if _, err := os.Stat(userPath); os.IsNotExist(err) {
os.WriteFile(userPath, []byte(userMDTemplate), 0644)
}
soulPath := filepath.Join(dir, "config", "soul.md")
if _, err := os.Stat(soulPath); os.IsNotExist(err) {
os.WriteFile(soulPath, []byte(soulMDTemplate), 0644)
}
}