init: 云枢·Agent 初始提交

This commit is contained in:
titor
2026-05-08 10:12:31 +08:00
commit a6025c699f
22 changed files with 2116 additions and 0 deletions

311
src/catalog.go Normal file
View File

@@ -0,0 +1,311 @@
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
// ============================================================
// YAML 目录结构
// ============================================================
type ToolsCatalog struct {
Version string `yaml:"version"`
Auto CatalogSection `yaml:"auto"`
Manual CatalogSection `yaml:"manual,omitempty"`
}
type CatalogSection struct {
Tools []CatalogTool `yaml:"tools"`
Skills []CatalogSkill `yaml:"skills"`
Agents []CatalogAgent `yaml:"agents"`
}
type CatalogTool struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Status string `yaml:"status"`
Description string `yaml:"description"`
Parameters map[string]ParameterField `yaml:"parameters"`
Returns string `yaml:"returns"`
Source string `yaml:"source"`
}
type ParameterField struct {
Type string `yaml:"type"`
Required bool `yaml:"required"`
Description string `yaml:"description"`
}
type CatalogSkill struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
Description string `yaml:"description"`
Status string `yaml:"status"`
}
type CatalogAgent struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
Description string `yaml:"description"`
Tools []string `yaml:"tools"`
Status string `yaml:"status"`
}
// ============================================================
// 构建目录
// ============================================================
func BuildCatalog() *ToolsCatalog {
return &ToolsCatalog{
Version: "1.0",
Auto: CatalogSection{
Tools: buildToolList(),
Skills: scanSkills(),
Agents: scanAgents(),
},
}
}
func buildToolList() []CatalogTool {
tools := ListRegisteredTools()
list := make([]CatalogTool, 0, len(tools))
for _, t := range tools {
ct := CatalogTool{
Name: t.Name,
Type: "builtin",
Status: "active",
Description: t.Description,
Source: "src/tool.go",
}
// 从 JSON Schema 提取参数
ct.Parameters = make(map[string]ParameterField)
for name, prop := range t.Parameters.Properties {
required := false
for _, r := range t.Parameters.Required {
if r == name {
required = true
break
}
}
ct.Parameters[name] = ParameterField{
Type: prop.Type,
Required: required,
Description: prop.Description,
}
}
list = append(list, ct)
}
return list
}
func scanSkills() []CatalogSkill {
// 搜索路径:项目目录 → 用户配置目录
dirs := []string{
"skills",
filepath.Join(ConfigDir(), "skills"),
}
seen := make(map[string]bool)
var list []CatalogSkill
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, e := range entries {
if !e.IsDir() || seen[e.Name()] {
continue
}
seen[e.Name()] = true
// 尝试读取 SKILL.md 获取描述
skillPath := filepath.Join(dir, e.Name(), "SKILL.md")
desc := fmt.Sprintf("skill: %s", e.Name())
if data, err := os.ReadFile(skillPath); err == nil {
content := string(data)
if fm, _, err := parseFrontmatterSimple(content); err == nil {
if d, ok := fm["description"]; ok {
desc = fmt.Sprintf("%v", d)
}
}
}
list = append(list, CatalogSkill{
Name: e.Name(),
Path: fmt.Sprintf("skills/%s/SKILL.md", e.Name()),
Description: desc,
Status: "active",
})
}
}
return list
}
func scanAgents() []CatalogAgent {
dirs := []string{
"agents",
filepath.Join(ConfigDir(), "agents"),
}
seen := make(map[string]bool)
var list []CatalogAgent
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") || seen[e.Name()] {
continue
}
seen[e.Name()] = true
agentPath := filepath.Join(dir, e.Name())
data, err := os.ReadFile(agentPath)
if err != nil {
continue
}
fm, _, err := parseFrontmatterSimple(string(data))
if err != nil {
continue
}
cat := CatalogAgent{
Name: fmt.Sprintf("%v", fm["name"]),
Path: fmt.Sprintf("agents/%s", e.Name()),
Status: "active",
}
if d, ok := fm["description"]; ok {
cat.Description = fmt.Sprintf("%v", d)
}
if tools, ok := fm["tools"]; ok {
if list, ok := tools.([]interface{}); ok {
for _, t := range list {
cat.Tools = append(cat.Tools, fmt.Sprintf("%v", t))
}
}
}
list = append(list, cat)
}
}
return list
}
// ============================================================
// 写入 tools.yml
// ============================================================
func GenerateToolsYAML() {
catalog := BuildCatalog()
// 尝试读取已有的 manual 节,保留用户编辑
existingPath := filepath.Join(ConfigDir(), "tools.yml")
if data, err := os.ReadFile(existingPath); err == nil {
var existing ToolsCatalog
if err := yaml.Unmarshal(data, &existing); err == nil {
catalog.Manual = existing.Manual
}
}
dir := ConfigDir()
if err := os.MkdirAll(dir, 0755); err != nil {
return
}
// 用 2 空格缩进编码
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
encoder.SetIndent(2)
if err := encoder.Encode(catalog); err != nil {
return
}
encoder.Close()
// 在文件头加注释
header := "# ⚠️ auto 节由云枢 Agent 自动生成每次启动覆写manual 节保留用户自定义\n"
yamlPath := filepath.Join(dir, "tools.yml")
os.WriteFile(yamlPath, []byte(header+buf.String()), 0644)
}
// ============================================================
// 注入目录到 system prompt
// ============================================================
// BuildInjectPrompt 生成能力边界目录,追加到 system prompt 末尾
func BuildInjectPrompt(toolNames []string) string {
var b strings.Builder
b.WriteString("\n\n## 能力边界\n\n")
// 工具
allTools := ListRegisteredTools()
b.WriteString("### 可用工具\n")
for _, t := range allTools {
available := false
for _, name := range toolNames {
if t.Name == name {
available = true
break
}
}
if available {
b.WriteString(fmt.Sprintf("- %s: %s\n", t.Name, t.Description))
}
}
// skill
skills := scanSkills()
if len(skills) > 0 {
b.WriteString("\n### 可用技能\n")
for _, s := range skills {
b.WriteString(fmt.Sprintf("- %s: %s\n", s.Name, s.Description))
}
}
// 数据文件
b.WriteString("\n### 数据文件\n")
b.WriteString("- data/tools.yml: 完整工具/技能/Agent 目录(可读此文件获取详细参数)\n")
return b.String()
}
// ============================================================
// 辅助:简易 frontmatter 解析(只读 YAML map
// ============================================================
func parseFrontmatterSimple(content string) (map[string]interface{}, string, error) {
lines := strings.Split(content, "\n")
if len(lines) < 2 || strings.TrimSpace(lines[0]) != "---" {
return nil, content, nil
}
endIdx := -1
for i := 1; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) == "---" {
endIdx = i
break
}
}
if endIdx == -1 {
return nil, content, nil
}
frontmatter := strings.Join(lines[1:endIdx], "\n")
body := strings.Join(lines[endIdx+1:], "\n")
var fm map[string]interface{}
if err := yaml.Unmarshal([]byte(frontmatter), &fm); err != nil {
return nil, body, nil
}
return fm, body, nil
}

157
src/config.go Normal file
View File

@@ -0,0 +1,157 @@
package main
import (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// Config 存储云枢 Agent 的所有配置
type Config struct {
LLM LLMConfig `yaml:"llm"`
}
// LLMConfig 大模型连接配置
type LLMConfig struct {
Host string `yaml:"host"`
Model string `yaml:"model"`
Key string `yaml:"key"`
}
// ConfigDir 返回全局配置目录路径 ~/.config/yunshu/
func ConfigDir() string {
home, err := os.UserHomeDir()
if err != nil {
return "./.config/yunshu"
}
return filepath.Join(home, ".config", "yunshu")
}
// oldConfigDir 旧版本配置目录,用于迁移
func oldConfigDir() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".config", "weather-cli")
}
// migrateOldConfig 从旧目录 weather-cli 迁移配置到 yunshu
func migrateOldConfig() {
old := oldConfigDir()
if old == "" {
return
}
// 旧目录不存在,无需迁移
if _, err := os.Stat(old); os.IsNotExist(err) {
return
}
// 新目录已存在,说明已迁移过
new := ConfigDir()
if _, err := os.Stat(new); err == nil {
return
}
// 复制旧目录到新目录
copyDirRecursive(old, new)
os.RemoveAll(old)
}
// LoadConfig 从 ~/.config/yunshu/config.yaml 读取配置
// 如果新目录不存在,自动从旧 weather-cli 目录迁移
func LoadConfig() (*Config, error) {
// 尝试自动迁移旧配置
migrateOldConfig()
path := filepath.Join(ConfigDir(), "config.yaml")
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// SaveConfig 将配置写入 ~/.config/yunshu/config.yaml
func SaveConfig(cfg *Config) error {
dir := ConfigDir()
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
path := filepath.Join(dir, "config.yaml")
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
// 0600 - 仅所有者可读写,保护 API Key
return os.WriteFile(path, data, 0600)
}
// SearchFile 在项目目录和全局配置目录中搜索文件
// 项目目录优先,全局配置目录作为后备
func SearchFile(relativePath string) string {
paths := []string{
relativePath, // 项目目录
filepath.Join(ConfigDir(), relativePath), // 全局配置目录
}
for _, p := range paths {
if _, err := os.Stat(p); err == nil {
return p
}
}
return relativePath
}
// CopyDefaultDir 将项目目录下的默认文件复制到全局配置目录
func CopyDefaultDir(srcDir, dstDir string) {
entries, err := os.ReadDir(srcDir)
if err != nil {
return
}
dstBase := filepath.Join(ConfigDir(), dstDir)
for _, e := range entries {
srcPath := filepath.Join(srcDir, e.Name())
dstPath := filepath.Join(dstBase, e.Name())
if e.IsDir() {
os.MkdirAll(dstPath, 0755)
copyDirRecursive(srcPath, dstPath)
} else {
data, err := os.ReadFile(srcPath)
if err != nil {
continue
}
os.MkdirAll(filepath.Dir(dstPath), 0755)
os.WriteFile(dstPath, data, 0644)
}
}
}
func copyDirRecursive(src, dst string) {
entries, err := os.ReadDir(src)
if err != nil {
return
}
os.MkdirAll(dst, 0755)
for _, e := range entries {
srcPath := filepath.Join(src, e.Name())
dstPath := filepath.Join(dst, e.Name())
if e.IsDir() {
copyDirRecursive(srcPath, dstPath)
} else {
data, err := os.ReadFile(srcPath)
if err != nil {
continue
}
os.WriteFile(dstPath, data, 0644)
}
}
}

130
src/llm.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
var (
llmHost = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
llmModel = "doubao-seed-2-0-pro-260215"
llmKey = ""
)
func init() {
// 1. 从配置文件加载
cfg, err := LoadConfig()
if err == nil {
if cfg.LLM.Host != "" {
llmHost = cfg.LLM.Host
}
if cfg.LLM.Model != "" {
llmModel = cfg.LLM.Model
}
if cfg.LLM.Key != "" {
llmKey = cfg.LLM.Key
}
}
// 2. 环境变量覆盖配置文件(优先级最高)
if v := os.Getenv("LLM_ENDPOINT"); v != "" {
llmHost = v
}
if v := os.Getenv("LLM_MODEL"); v != "" {
llmModel = v
}
if v := os.Getenv("LLM_API_KEY"); v != "" {
llmKey = v
}
// 兼容旧环境变量名
if v := os.Getenv("OPENAI_API_KEY"); v != "" && llmKey == "" {
llmKey = v
}
}
// GetLLMKey 获取 API Key优先使用已加载的密钥
func GetLLMKey() (string, error) {
if llmKey == "" {
return "", fmt.Errorf("未配置 API Key。请运行 'weather-cia onboard' 初始化,或设置 LLM_API_KEY 环境变量")
}
return llmKey, nil
}
// CallLLM 调用大模型 API兼容 OpenAI Chat Completion 格式)
func CallLLM(messages []Message, toolDefs []ToolDef) (*OpenAIResponse, error) {
apiKey, err := GetLLMKey()
if err != nil {
return nil, err
}
reqBody := map[string]interface{}{
"model": llmModel,
"messages": messages,
}
// 注册工具定义
if len(toolDefs) > 0 {
tools := make([]OpenAITool, 0, len(toolDefs))
for _, td := range toolDefs {
tools = append(tools, OpenAITool{
Type: "function",
Function: OpenAIToolFunc{
Name: td.Name,
Description: td.Description,
Parameters: td.Parameters,
},
})
}
reqBody["tools"] = tools
reqBody["tool_choice"] = "auto"
}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("序列化请求失败: %w", err)
}
req, err := http.NewRequest("POST", llmHost, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{Timeout: 120 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求 LLM 失败: %w", err)
}
defer resp.Body.Close()
respData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode != 200 {
var errResp OpenAIErrorResponse
if json.Unmarshal(respData, &errResp) == nil && errResp.Error.Message != "" {
return nil, fmt.Errorf("LLM API 错误 [%s]: %s", errResp.Error.Type, errResp.Error.Message)
}
return nil, fmt.Errorf("LLM API 返回 HTTP %d: %s", resp.StatusCode, string(respData))
}
var result OpenAIResponse
if err := json.Unmarshal(respData, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
if len(result.Choices) == 0 {
return nil, fmt.Errorf("LLM 返回空结果")
}
return &result, nil
}

76
src/loader.go Normal file
View File

@@ -0,0 +1,76 @@
package main
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
// LoadAgent 从 .md 文件加载 agent 定义
// 搜索结果:项目目录 → 全局配置目录
func LoadAgent(name string) (*AgentDef, error) {
path := SearchFile(name)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取 agent 文件失败 (搜索路径: %s): %w", name, err)
}
content := string(data)
frontmatter, body, err := parseFrontmatter(content)
if err != nil {
return nil, err
}
def := &AgentDef{SystemPrompt: strings.TrimSpace(body)}
if err := yaml.Unmarshal([]byte(frontmatter), def); err != nil {
return nil, fmt.Errorf("解析 frontmatter 失败: %w", err)
}
if def.Name == "" {
return nil, fmt.Errorf("agent 定义缺少 name 字段")
}
// 注入能力边界目录到 system prompt
def.SystemPrompt += BuildInjectPrompt(def.Tools)
return def, nil
}
func parseFrontmatter(content string) (string, string, error) {
lines := strings.Split(content, "\n")
if len(lines) < 2 || strings.TrimSpace(lines[0]) != "---" {
return "", content, nil
}
endIdx := -1
for i := 1; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) == "---" {
endIdx = i
break
}
}
if endIdx == -1 {
return "", content, nil
}
frontmatter := strings.Join(lines[1:endIdx], "\n")
body := strings.Join(lines[endIdx+1:], "\n")
return frontmatter, body, nil
}
// LoadSkill 按名称加载 skill 知识内容
// 搜索结果skills/<name>/SKILL.md → ~/.config/weather-cli/skills/<name>/SKILL.md
func LoadSkill(name string) (string, error) {
relativePath := fmt.Sprintf("skills/%s/SKILL.md", name)
path := SearchFile(relativePath)
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("skill %q 未找到", name)
}
_, body, _ := parseFrontmatter(string(data))
return strings.TrimSpace(body), nil
}

85
src/main.go Normal file
View File

@@ -0,0 +1,85 @@
package main
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
)
func init() {
// Windows 控制台 UTF-8 编码
kernel32 := syscall.NewLazyDLL("kernel32.dll")
setConsoleCP := kernel32.NewProc("SetConsoleOutputCP")
setConsoleCP.Call(65001)
}
func main() {
args := os.Args[1:]
// onboard 子命令:交互式初始化
if len(args) > 0 && args[0] == "onboard" {
runOnboard()
return
}
// 加载配置
cfg, err := LoadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "❌ 未找到配置文件。请先运行:\n")
fmt.Fprintf(os.Stderr, " yunshu onboard\n")
os.Exit(1)
}
// 配置已通过 init() 加载到 llmHost/llmModel/llmKey 中
_ = cfg
// 生成工具目录(启动时覆写 auto 节,保留 manual 节)
GenerateToolsYAML()
// 查找并加载 agent 定义
agentPath := SearchFile("agents/weather-agent.md")
def, err := LoadAgent(agentPath)
if err != nil {
fmt.Fprintf(os.Stderr, "加载 agent 失败: %v\n", err)
os.Exit(1)
}
// 单次查询模式
if len(args) > 0 {
ClearSession()
query := strings.Join(args, " ")
if err := RunAgent(def, query); err != nil {
fmt.Fprintf(os.Stderr, "错误: %v\n", err)
os.Exit(1)
}
return
}
// 交互模式
fmt.Println("☁️ 云枢 Agent — 天气情报官,输入 exit 退出")
fmt.Println(strings.Repeat("─", 50))
ClearSession()
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("> ")
if !scanner.Scan() {
break
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
if input == "exit" || input == "quit" {
fmt.Println("再见!")
break
}
if err := RunAgent(def, input); err != nil {
fmt.Fprintf(os.Stderr, "错误: %v\n", err)
}
fmt.Println(strings.Repeat("─", 50))
}
}

69
src/onboard.go Normal file
View File

@@ -0,0 +1,69 @@
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
)
// runOnboard 交互式初始化向导
func runOnboard() {
fmt.Println(strings.Repeat("=", 50))
fmt.Println(" ☁️ 云枢 Agent 初始化配置")
fmt.Println(strings.Repeat("=", 50))
reader := bufio.NewReader(os.Stdin)
// 读取 LLM 地址
defaultHost := "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
fmt.Printf("\nLLM 接口地址\n 默认: %s\n > ", defaultHost)
host, _ := reader.ReadString('\n')
host = strings.TrimSpace(host)
if host == "" {
host = defaultHost
}
// 读取 API Key
fmt.Print("API Key\n > ")
key, _ := reader.ReadString('\n')
key = strings.TrimSpace(key)
// 读取模型名称
defaultModel := "doubao-seed-2-0-pro-260215"
fmt.Printf("模型名称\n 默认: %s\n > ", defaultModel)
model, _ := reader.ReadString('\n')
model = strings.TrimSpace(model)
if model == "" {
model = defaultModel
}
// 保存配置
cfg := &Config{
LLM: LLMConfig{
Host: host,
Model: model,
Key: key,
},
}
if err := SaveConfig(cfg); err != nil {
fmt.Fprintf(os.Stderr, "保存配置失败: %v\n", err)
os.Exit(1)
}
// 复制默认 agents / skills / data 到全局配置目录
CopyDefaultDir("agents", "agents")
CopyDefaultDir("skills", "skills")
CopyDefaultDir("data", "data")
configPath := filepath.Join(ConfigDir(), "config.yaml")
fmt.Printf("\n✅ 配置完成!\n")
fmt.Printf(" 配置文件: %s\n", configPath)
fmt.Printf(" Agent 目录: %s\n", filepath.Join(ConfigDir(), "agents"))
fmt.Println()
fmt.Println("运行示例:")
fmt.Println(" yunshu \"北京今天天气\"")
fmt.Println(" yunshu")
}

101
src/runtime.go Normal file
View File

@@ -0,0 +1,101 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
func sessionPath() string {
return filepath.Join(ConfigDir(), "session.json")
}
func ClearSession() {
os.Remove(sessionPath())
}
func LoadSession() []Message {
data, err := os.ReadFile(sessionPath())
if err != nil {
return nil
}
var messages []Message
if err := json.Unmarshal(data, &messages); err != nil {
return nil
}
return messages
}
func AppendToSession(msg Message) {
messages := LoadSession()
messages = append(messages, msg)
data, err := json.MarshalIndent(messages, "", " ")
if err != nil {
return
}
os.WriteFile(sessionPath(), data, 0644)
}
func RunAgent(def *AgentDef, userInput string) error {
messages := LoadSession()
fullMessages := []Message{
{Role: RoleSystem, Content: def.SystemPrompt},
}
fullMessages = append(fullMessages, messages...)
fullMessages = append(fullMessages, Message{Role: RoleUser, Content: userInput})
AppendToSession(Message{Role: RoleUser, Content: userInput})
toolDefs := GetToolDefs(def.Tools)
for {
resp, err := CallLLM(fullMessages, toolDefs)
if err != nil {
return err
}
choice := resp.Choices[0]
if len(choice.Message.ToolCalls) > 0 {
assistantMsg := Message{
Role: RoleAssistant,
ToolCalls: choice.Message.ToolCalls,
}
fullMessages = append(fullMessages, assistantMsg)
AppendToSession(assistantMsg)
for _, tc := range choice.Message.ToolCalls {
result, err := ExecuteTool(tc)
if err != nil {
result = fmt.Sprintf("工具执行错误: %v", err)
}
toolMsg := Message{
Role: RoleTool,
Content: result,
ToolCallID: tc.ID,
}
fullMessages = append(fullMessages, toolMsg)
AppendToSession(toolMsg)
}
} else {
content := ""
if choice.Message.Content != nil {
content = *choice.Message.Content
}
assistantMsg := Message{
Role: RoleAssistant,
Content: content,
}
fullMessages = append(fullMessages, assistantMsg)
AppendToSession(assistantMsg)
fmt.Println(content)
return nil
}
}
}

224
src/tool.go Normal file
View File

@@ -0,0 +1,224 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
var registeredTools = make(map[string]*ToolDef)
func RegisterTool(td *ToolDef) {
registeredTools[td.Name] = td
}
func ListRegisteredTools() []*ToolDef {
list := make([]*ToolDef, 0, len(registeredTools))
for _, td := range registeredTools {
list = append(list, td)
}
return list
}
func GetToolDefs(names []string) []ToolDef {
defs := make([]ToolDef, 0, len(names))
for _, name := range names {
if td, ok := registeredTools[name]; ok {
defs = append(defs, *td)
}
}
return defs
}
func ExecuteTool(tc ToolCall) (string, error) {
td, ok := registeredTools[tc.Function.Name]
if !ok {
return "", fmt.Errorf("未知工具: %s", tc.Function.Name)
}
var args map[string]interface{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
return "", fmt.Errorf("解析工具参数失败: %w", err)
}
return td.Execute(args)
}
func init() {
RegisterTool(&ToolDef{
Name: "http-get",
Description: "发送 HTTP GET 请求获取数据",
Parameters: ToolParameter{
Type: "object",
Properties: map[string]ToolProperty{
"url": {Type: "string", Description: "请求的完整 URL 地址"},
"headers": {Type: "string", Description: "请求头JSON 格式的键值对字符串,如 {\"User-Agent\": \"...\"}"},
},
Required: []string{"url"},
},
Execute: func(args map[string]interface{}) (string, error) {
url, _ := args["url"].(string)
if url == "" {
return "", fmt.Errorf("缺少 url 参数")
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", fmt.Errorf("创建请求失败: %w", err)
}
if headersStr, ok := args["headers"].(string); ok && headersStr != "" {
var headers map[string]string
if err := json.Unmarshal([]byte(headersStr), &headers); err == nil {
for k, v := range headers {
req.Header.Set(k, v)
}
}
}
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode != 200 {
return fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(body)), nil
}
return string(body), nil
},
})
RegisterTool(&ToolDef{
Name: "skill",
Description: "加载指定名称的 Skill 知识内容到当前上下文,获取专业知识",
Parameters: ToolParameter{
Type: "object",
Properties: map[string]ToolProperty{
"name": {Type: "string", Description: "Skill 名称,如 msn-weather-api"},
},
Required: []string{"name"},
},
Execute: func(args map[string]interface{}) (string, error) {
name, _ := args["name"].(string)
if name == "" {
return "", fmt.Errorf("缺少 name 参数")
}
return LoadSkill(name)
},
})
RegisterTool(&ToolDef{
Name: "read-file",
Description: "读取本地文件内容",
Parameters: ToolParameter{
Type: "object",
Properties: map[string]ToolProperty{
"path": {Type: "string", Description: "文件路径,相对于项目根目录"},
},
Required: []string{"path"},
},
Execute: func(args map[string]interface{}) (string, error) {
path, _ := args["path"].(string)
if path == "" {
return "", fmt.Errorf("缺少 path 参数")
}
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("读取文件失败: %w", err)
}
return strings.TrimSpace(string(data)), nil
},
})
RegisterTool(&ToolDef{
Name: "geocode",
Description: "查询城市或地点的经纬度坐标,返回 lat/lon/name/country。支持中文城市名如 北京、上海、成都)和英文名",
Parameters: ToolParameter{
Type: "object",
Properties: map[string]ToolProperty{
"city": {Type: "string", Description: "城市名称,支持中文(如 北京)或英文(如 Beijing"},
},
Required: []string{"city"},
},
Execute: func(args map[string]interface{}) (string, error) {
city, _ := args["city"].(string)
if city == "" {
return "", fmt.Errorf("缺少 city 参数")
}
url := fmt.Sprintf("https://wttr.in/%s?format=j1", city)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", fmt.Errorf("创建请求失败: %w", err)
}
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("请求 wttr.in 失败: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("wttr.in 返回 HTTP %d: %s", resp.StatusCode, string(body))
}
var result struct {
NearestArea []struct {
AreaName []struct {
Value string `json:"value"`
} `json:"areaName"`
Country []struct {
Value string `json:"value"`
} `json:"country"`
Latitude string `json:"latitude"`
Longitude string `json:"longitude"`
Population string `json:"population"`
} `json:"nearest_area"`
}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析 wttr.in 响应失败: %w", err)
}
if len(result.NearestArea) == 0 {
return "", fmt.Errorf("未找到城市: %s", city)
}
area := result.NearestArea[0]
name := ""
if len(area.AreaName) > 0 {
name = area.AreaName[0].Value
}
country := ""
if len(area.Country) > 0 {
country = area.Country[0].Value
}
out, _ := json.Marshal(map[string]interface{}{
"lat": area.Latitude,
"lon": area.Longitude,
"name": name,
"country": country,
})
return string(out), nil
},
})
}

101
src/types.go Normal file
View File

@@ -0,0 +1,101 @@
package main
type MessageRole string
const (
RoleSystem MessageRole = "system"
RoleUser MessageRole = "user"
RoleAssistant MessageRole = "assistant"
RoleTool MessageRole = "tool"
)
type Message struct {
Role MessageRole `json:"role"`
Content string `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function ToolCallFunction `json:"function"`
}
type ToolCallFunction struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
type AgentDef struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Tools []string `yaml:"tools"`
SystemPrompt string
}
type ToolParameter struct {
Type string `json:"type"`
Properties map[string]ToolProperty `json:"properties"`
Required []string `json:"required"`
}
type ToolProperty struct {
Type string `json:"type"`
Description string `json:"description"`
}
type ToolDef struct {
Name string
Description string
Parameters ToolParameter
Execute func(args map[string]interface{}) (string, error)
}
type OpenAIMessage struct {
Role string `json:"role"`
Content *string `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
}
type OpenAIChoice struct {
Index int `json:"index"`
Message OpenAIMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}
type OpenAIUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
type OpenAIResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []OpenAIChoice `json:"choices"`
Usage OpenAIUsage `json:"usage"`
}
type OpenAIErrorResponse struct {
Error OpenAIError `json:"error"`
}
type OpenAIError struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code"`
}
type OpenAITool struct {
Type string `json:"type"`
Function OpenAIToolFunc `json:"function"`
}
type OpenAIToolFunc struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters interface{} `json:"parameters"`
}