Files
homeagent-sdk/example/music/plugin.go
JianFeeeee cb7999ca71 feat: sdk NoMemory/Cleaner + example build fixes
- sdk/plugin.go: ToolDef adds NoMemory/Cleaner fields
- sdk/plugin_test.go: unit tests for NoMemory/Cleaner
- all example plugins: NoMemory/Cleaner annotated for each tool
- plugindev/templates.go: template shows NoMemory/Cleaner pattern
- plugindev/cmd_build.go: fix ensureGoMod, buildBundle/buildTarget sdkPath param, NewPluginFactory
- example/go.mod: add external dependency declarations (chromedp, gofeed)
- add main.go stubs for all example plugins
2026-07-25 11:17:21 +08:00

328 lines
7.6 KiB
Go
Raw Permalink 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 (
"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 NewPluginFactory(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"},
},
Cleaner: func(output string) string {
var r struct{ Content string }
if json.Unmarshal([]byte(output), &r) == nil && r.Content != "" {
return r.Content
}
return output
},
}, 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")
}