feat: add example plugins (ai_image, calendar, music, rss, weather), fix .gitignore, move gengskill to tools/

This commit is contained in:
root
2026-07-21 12:45:17 +08:00
parent 75ae2b4692
commit 52dc22f86f
54 changed files with 13650 additions and 202 deletions

320
example/music/plugin.go Normal file
View File

@ -0,0 +1,320 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
type Plugin struct {
name string
sdk *sdk.PluginSDK
cli *http.Client
}
type searchResp struct {
Result *struct {
Songs []songItem `json:"songs"`
SongCount int `json:"songCount"`
} `json:"result"`
Code int `json:"code"`
}
type songItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Artists []artist `json:"artists"`
Album albumInfo `json:"album"`
Duration int `json:"duration"`
Mvid int `json:"mvid"`
Fee int `json:"fee"`
}
type artist struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type albumInfo struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type lyricResp struct {
Lrc *lyricData `json:"lrc"`
TLrc *lyricData `json:"tlyric"`
Code int `json:"code"`
}
type lyricData struct {
Lyric string `json:"lyric"`
}
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.SetAutoRestart(true)
p.sdk = s
p.cli = &http.Client{Timeout: 15 * time.Second}
s.RegisterTool(p.name+"_search", sdk.ToolDef{
Name: p.name + "_search",
Description: "搜索歌曲,通过关键词查找音乐,返回歌曲列表",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"keyword": map[string]interface{}{
"type": "string",
"description": "搜索关键词,如歌曲名、歌手名",
},
"limit": map[string]interface{}{
"type": "integer",
"description": "返回结果数量1-50默认10",
},
},
"required": []string{"keyword"},
},
}, p.handleSearch)
s.RegisterTool(p.name+"_lyrics", sdk.ToolDef{
Name: p.name + "_lyrics",
Description: "获取歌曲歌词通过歌曲ID查看歌词内容",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"song_id": map[string]interface{}{
"type": "integer",
"description": "歌曲ID从搜索结果的 id 字段获取)",
},
},
"required": []string{"song_id"},
},
}, p.handleLyrics)
return nil
}
func (p *Plugin) Stop() error { return nil }
func (p *Plugin) neRequest(path string, params map[string]string) ([]byte, error) {
base := "https://music.163.com/api" + path
reqURL := base + "?" + urlValues(params).Encode()
req, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
req.Header.Set("Referer", "https://music.163.com/")
resp, err := p.cli.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func urlValues(m map[string]string) url.Values {
v := url.Values{}
for k, val := range m {
v.Set(k, val)
}
return v
}
func (p *Plugin) handleSearch(args map[string]interface{}) (interface{}, error) {
keyword, _ := args["keyword"].(string)
keyword = strings.TrimSpace(keyword)
if keyword == "" {
return map[string]interface{}{
"content": "请输入搜索关键词",
"isError": true,
}, nil
}
limit := 10
if v, ok := args["limit"].(float64); ok {
limit = int(v)
if limit < 1 {
limit = 1
}
if limit > 50 {
limit = 50
}
}
body, err := p.neRequest("/search/get", map[string]string{
"s": keyword,
"type": "1",
"limit": fmt.Sprint(limit),
})
if err != nil {
return map[string]interface{}{
"content": fmt.Sprintf("搜索失败:%v", err),
"isError": true,
}, nil
}
var resp searchResp
if err := json.Unmarshal(body, &resp); err != nil {
return map[string]interface{}{
"content": fmt.Sprintf("解析响应失败:%v", err),
"isError": true,
}, nil
}
if resp.Code != 200 || resp.Result == nil {
return map[string]interface{}{
"content": fmt.Sprintf("搜索失败,响应码:%d", resp.Code),
"isError": true,
}, nil
}
songs := resp.Result.Songs
if len(songs) == 0 {
return map[string]interface{}{
"content": fmt.Sprintf("未找到与「%s」相关的歌曲", keyword),
}, nil
}
var lines []string
lines = append(lines, fmt.Sprintf("找到 %d 首与「%s」相关的歌曲\n", resp.Result.SongCount, keyword))
for i, s := range songs {
var artists []string
for _, a := range s.Artists {
artists = append(artists, a.Name)
}
dur := time.Duration(s.Duration) * time.Millisecond
minutes := int(dur.Minutes())
seconds := int(dur.Seconds()) % 60
lines = append(lines, fmt.Sprintf("%d. %s - %s [%02d:%02d] (ID: %d)",
i+1, s.Name, strings.Join(artists, "/"), minutes, seconds, s.ID))
}
type songResult struct {
ID int64 `json:"id"`
Name string `json:"name"`
Artists []string `json:"artists"`
Album string `json:"album"`
Duration int `json:"duration"`
}
var results []songResult
for _, s := range songs {
var artists []string
for _, a := range s.Artists {
artists = append(artists, a.Name)
}
results = append(results, songResult{
ID: s.ID,
Name: s.Name,
Artists: artists,
Album: s.Album.Name,
Duration: s.Duration,
})
}
return map[string]interface{}{
"content": strings.Join(lines, "\n"),
"songs": results,
"total": resp.Result.SongCount,
}, nil
}
func (p *Plugin) handleLyrics(args map[string]interface{}) (interface{}, error) {
songID, ok := args["song_id"].(float64)
if !ok {
return map[string]interface{}{
"content": "请提供有效的歌曲ID",
"isError": true,
}, nil
}
id := int64(songID)
body, err := p.neRequest("/song/lyric", map[string]string{
"id": fmt.Sprint(id),
"lv": "-1",
"kv": "-1",
"tv": "-1",
})
if err != nil {
return map[string]interface{}{
"content": fmt.Sprintf("获取歌词失败:%v", err),
"isError": true,
}, nil
}
var resp lyricResp
if err := json.Unmarshal(body, &resp); err != nil {
return map[string]interface{}{
"content": fmt.Sprintf("解析歌词失败:%v", err),
"isError": true,
}, nil
}
if resp.Code != 200 {
return map[string]interface{}{
"content": fmt.Sprintf("获取歌词失败,响应码:%d", resp.Code),
"isError": true,
}, nil
}
lyric := ""
if resp.Lrc != nil {
lyric = resp.Lrc.Lyric
}
if lyric == "" {
return map[string]interface{}{
"content": fmt.Sprintf("歌曲 %d 暂无歌词", id),
}, nil
}
// Clean up lyrics metadata lines and limit length
lyric = cleanLyrics(lyric)
if len(lyric) > 3000 {
lyric = lyric[:3000] + "\n...(歌词过长已截断)"
}
tLyric := ""
if resp.TLrc != nil && resp.TLrc.Lyric != "" {
tLyric = cleanLyrics(resp.TLrc.Lyric)
if len(tLyric) > 1000 {
tLyric = tLyric[:1000] + "\n...(翻译过长已截断)"
}
}
result := fmt.Sprintf("歌词:\n%s", lyric)
if tLyric != "" {
result += fmt.Sprintf("\n翻译\n%s", tLyric)
}
return map[string]interface{}{
"content": result,
"lyric": lyric,
"tlyric": tLyric,
}, nil
}
func cleanLyrics(l string) string {
lines := strings.Split(l, "\n")
var cleaned []string
for _, line := range lines {
// Skip metadata lines like [ti:...], [ar:...], [al:...], [by:...], [offset:...]
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
cleaned = append(cleaned, line)
}
return strings.Join(cleaned, "\n")
}