- 模块名重命名 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*
225 lines
5.8 KiB
Go
225 lines
5.8 KiB
Go
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
|
||
},
|
||
})
|
||
}
|