mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 17:08:01 +00:00
feat: add example plugins (ai_image, calendar, music, rss, weather), fix .gitignore, move gengskill to tools/
This commit is contained in:
13
example/ai_image/README.md
Normal file
13
example/ai_image/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# ai_image
|
||||
|
||||
ai_image plugin
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
plugindev build
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Upload the .hmap file through the Plugin Manager API.
|
||||
7
example/ai_image/go.mod
Normal file
7
example/ai_image/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module ai_image
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone
|
||||
11
example/ai_image/plg.json
Normal file
11
example/ai_image/plg.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "ai_image",
|
||||
"name_zh": "AI绘图",
|
||||
"name_en": "AI Image",
|
||||
"version": "1.0.0",
|
||||
"description": "AI 图像生成插件,支持 OpenAI DALL·E / Stable Diffusion",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": ["ai", "image", "draw", "generate"],
|
||||
"targets": "linux/amd64"
|
||||
}
|
||||
344
example/ai_image/plugin.go
Normal file
344
example/ai_image/plugin.go
Normal file
@ -0,0 +1,344 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
sdk *sdk.PluginSDK
|
||||
client *http.Client
|
||||
apiKey string
|
||||
provider string
|
||||
model string
|
||||
size string
|
||||
}
|
||||
|
||||
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 readCfg[T string | int64 | float64](s sdk.SettingsAPI, key string, fallback T) T {
|
||||
v, err := s.Get(key)
|
||||
if err == nil && v != nil {
|
||||
if sv, ok := v.(string); ok && sv != "" {
|
||||
switch any(fallback).(type) {
|
||||
case string:
|
||||
return any(sv).(T)
|
||||
case int64:
|
||||
if n, err := strconv.ParseInt(sv, 10, 64); err == nil {
|
||||
return any(n).(T)
|
||||
}
|
||||
case float64:
|
||||
if n, err := strconv.ParseFloat(sv, 64); err == nil {
|
||||
return any(n).(T)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v2, err2 := s.GetCore("plugin." + "ai_image" + "." + key)
|
||||
if err2 == nil && v2 != nil {
|
||||
if sv, ok := v2.(string); ok && sv != "" {
|
||||
switch any(fallback).(type) {
|
||||
case string:
|
||||
return any(sv).(T)
|
||||
case int64:
|
||||
if n, err := strconv.ParseInt(sv, 10, 64); err == nil {
|
||||
return any(n).(T)
|
||||
}
|
||||
case float64:
|
||||
if n, err := strconv.ParseFloat(sv, 64); err == nil {
|
||||
return any(n).(T)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func readArg[T string | int64 | float64](args map[string]interface{}, key string, fallback T) T {
|
||||
v, ok := args[key]
|
||||
if !ok || v == nil {
|
||||
return fallback
|
||||
}
|
||||
switch any(fallback).(type) {
|
||||
case string:
|
||||
if s, ok := v.(string); ok {
|
||||
return any(s).(T)
|
||||
}
|
||||
case int64:
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return any(int64(n)).(T)
|
||||
case int64:
|
||||
return any(n).(T)
|
||||
case string:
|
||||
if i, err := strconv.ParseInt(n, 10, 64); err == nil {
|
||||
return any(i).(T)
|
||||
}
|
||||
}
|
||||
case float64:
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return any(n).(T)
|
||||
case int64:
|
||||
return any(float64(n)).(T)
|
||||
case string:
|
||||
if f, err := strconv.ParseFloat(n, 64); err == nil {
|
||||
return any(f).(T)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.sdk = s
|
||||
|
||||
p.apiKey = readCfg(s.Settings(), "api_key", "")
|
||||
p.provider = readCfg(s.Settings(), "provider", "openai")
|
||||
p.model = readCfg(s.Settings(), "model", "dall-e-3")
|
||||
p.size = readCfg(s.Settings(), "size", "1024x1024")
|
||||
|
||||
p.client = &http.Client{Timeout: 120 * time.Second}
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin.ai_image.api_key", Default: "", Type: "string",
|
||||
DisplayName: "API Key", Description: "OpenAI / Stable Diffusion API Key",
|
||||
Category: "ai_image", Secret: true,
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin.ai_image.provider", Default: "openai", Type: "string",
|
||||
DisplayName: "Provider", Description: "Image generation provider: openai / stability",
|
||||
Category: "ai_image",
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin.ai_image.model", Default: "dall-e-3", Type: "string",
|
||||
DisplayName: "Model", Description: "Model name (dall-e-3, sd-xl, etc.)",
|
||||
Category: "ai_image",
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin.ai_image.size", Default: "1024x1024", Type: "string",
|
||||
DisplayName: "Size", Description: "Default image size (1024x1024, 1024x1792, 1792x1024)",
|
||||
Category: "ai_image",
|
||||
})
|
||||
|
||||
tp := p.name + "_"
|
||||
s.RegisterTool(tp+"generate", sdk.ToolDef{
|
||||
Name: tp + "generate", Description: "Generate image from text prompt using AI. Returns image URL.",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"prompt": map[string]interface{}{"type": "string", "description": "Text description of the image to generate"},
|
||||
"size": map[string]interface{}{"type": "string", "description": "Image size (1024x1024, 1024x1792, 1792x1024), default from config"},
|
||||
"model": map[string]interface{}{"type": "string", "description": "Model override (dall-e-3, dall-e-2)"},
|
||||
"n": map[string]interface{}{"type": "integer", "description": "Number of images to generate (1-10), default 1"},
|
||||
},
|
||||
"required": []string{"prompt"},
|
||||
},
|
||||
}, p.handleGenerate)
|
||||
|
||||
fmt.Printf("[%s] started (provider=%s, model=%s)\n", p.name, p.provider, p.model)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
fmt.Printf("[%s] stopped\n", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
type openAIReq struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Size string `json:"size"`
|
||||
ResponseFormat string `json:"response_format"`
|
||||
}
|
||||
|
||||
type openAIResp struct {
|
||||
Created int64 `json:"created"`
|
||||
Data []struct {
|
||||
RevisedPrompt string `json:"revised_prompt"`
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (p *Plugin) handleGenerate(args map[string]interface{}) (interface{}, error) {
|
||||
prompt := readArg(args, "prompt", "")
|
||||
if prompt == "" {
|
||||
return map[string]interface{}{"isError": true, "content": "prompt is required"}, nil
|
||||
}
|
||||
|
||||
p.apiKey = readCfg(p.sdk.Settings(), "api_key", p.apiKey)
|
||||
if p.apiKey == "" {
|
||||
return map[string]interface{}{"isError": true, "content": "API key not configured. Set plugin.ai_image.api_key via CLI."}, nil
|
||||
}
|
||||
|
||||
provider := readCfg(p.sdk.Settings(), "provider", p.provider)
|
||||
model := readArg(args, "model", readCfg(p.sdk.Settings(), "model", p.model))
|
||||
size := readArg(args, "size", readCfg(p.sdk.Settings(), "size", p.size))
|
||||
n := readArg(args, "n", int64(1))
|
||||
if n < 1 {
|
||||
n = 1
|
||||
}
|
||||
if n > 10 {
|
||||
n = 10
|
||||
}
|
||||
|
||||
switch provider {
|
||||
case "openai":
|
||||
return p.generateOpenAI(prompt, model, size, int(n))
|
||||
case "stability":
|
||||
return p.generateStability(prompt, model, size, int(n))
|
||||
default:
|
||||
return map[string]interface{}{"isError": true, "content": "Unknown provider: " + provider + ". Supported: openai, stability"}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) generateOpenAI(prompt, model, size string, n int) (interface{}, error) {
|
||||
body := openAIReq{
|
||||
Model: model,
|
||||
Prompt: prompt,
|
||||
N: n,
|
||||
Size: size,
|
||||
ResponseFormat: "url",
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "https://api.openai.com/v1/images/generations", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"isError": true, "content": "Request failed: " + err.Error()}, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
var result openAIResp
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return map[string]interface{}{"isError": true, "content": "Failed to parse response: " + err.Error()}, nil
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
return map[string]interface{}{"isError": true, "content": "API error: " + result.Error.Message}, nil
|
||||
}
|
||||
|
||||
if len(result.Data) == 0 {
|
||||
return map[string]interface{}{"isError": true, "content": "No images returned"}, nil
|
||||
}
|
||||
|
||||
urls := make([]string, len(result.Data))
|
||||
for i, d := range result.Data {
|
||||
urls[i] = d.URL
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"content": fmt.Sprintf("Generated %d image(s) with model %s:\n%s", len(urls), model, strings.Join(urls, "\n")),
|
||||
"images": urls,
|
||||
"prompt": prompt,
|
||||
"model": model,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type stabilityReq struct {
|
||||
TextPrompts []stabilityPrompt `json:"text_prompts"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Samples int `json:"samples"`
|
||||
}
|
||||
|
||||
type stabilityPrompt struct {
|
||||
Text string `json:"text"`
|
||||
Weight float64 `json:"weight,omitempty"`
|
||||
}
|
||||
|
||||
type stabilityArtifact struct {
|
||||
Base64 string `json:"base64"`
|
||||
Seed int `json:"seed"`
|
||||
}
|
||||
|
||||
type stabilityResp struct {
|
||||
Artifacts []stabilityArtifact `json:"artifacts"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Plugin) generateStability(prompt, model, size string, n int) (interface{}, error) {
|
||||
width, height := 1024, 1024
|
||||
if parts := strings.Split(size, "x"); len(parts) == 2 {
|
||||
if w, err := strconv.Atoi(parts[0]); err == nil {
|
||||
width = w
|
||||
}
|
||||
if h, err := strconv.Atoi(parts[1]); err == nil {
|
||||
height = h
|
||||
}
|
||||
}
|
||||
|
||||
body := stabilityReq{
|
||||
TextPrompts: []stabilityPrompt{{Text: prompt, Weight: 1.0}},
|
||||
Width: width,
|
||||
Height: height,
|
||||
Samples: n,
|
||||
}
|
||||
|
||||
apiURL := "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image"
|
||||
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", apiURL, bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"isError": true, "content": "Request failed: " + err.Error()}, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return map[string]interface{}{"isError": true, "content": fmt.Sprintf("API error (status %d): %s", resp.StatusCode, string(respBody))}, nil
|
||||
}
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
var result stabilityResp
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return map[string]interface{}{"isError": true, "content": "Failed to parse response: " + err.Error()}, nil
|
||||
}
|
||||
|
||||
if len(result.Artifacts) == 0 {
|
||||
msg := result.Message
|
||||
if msg == "" {
|
||||
msg = "No images returned"
|
||||
}
|
||||
return map[string]interface{}{"isError": true, "content": msg}, nil
|
||||
}
|
||||
|
||||
urls := make([]string, len(result.Artifacts))
|
||||
for i, a := range result.Artifacts {
|
||||
urls[i] = "data:image/png;base64," + a.Base64
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"content": fmt.Sprintf("Generated %d image(s) via Stability AI:\n%s", len(urls), strings.Join(urls, "\n")),
|
||||
"images": urls,
|
||||
"prompt": prompt,
|
||||
"model": model,
|
||||
}, nil
|
||||
}
|
||||
@ -1,7 +1,21 @@
|
||||
module browser
|
||||
module browser-plugin
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0-20260708004841-e9bdcf9304b0
|
||||
require (
|
||||
gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||
github.com/chromedp/chromedp v0.9.5
|
||||
)
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => ../../.
|
||||
require (
|
||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732 // indirect
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.3.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
)
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone
|
||||
|
||||
23
example/browser/go.sum
Normal file
23
example/browser/go.sum
Normal file
@ -0,0 +1,23 @@
|
||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732 h1:XYUCaZrW8ckGWlCRJKCSoh/iFwlpX316a8yY9IFEzv8=
|
||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
|
||||
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
|
||||
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "browser",
|
||||
"name_zh": "浏览器",
|
||||
"name_en": "browser",
|
||||
"version": "1.0.0",
|
||||
"description": "网络资源搜索与获取:搜索引擎查询(browser_search)、网页抓取(browser_fetch,SSRF防护)、无头浏览器渲染(browser_render)",
|
||||
"name_en": "Browser",
|
||||
"version": "2.0.0",
|
||||
"description": "统一浏览器插件:搜索、HTTP抓取(quick)、无头渲染(normal)、交互式浏览器(interactive/CDP)",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": ["web", "search", "fetch", "browser"],
|
||||
"tags": ["web", "search", "fetch", "browser", "cdp"],
|
||||
"targets": "linux/amd64"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
13
example/calendar/README.md
Normal file
13
example/calendar/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# calendar
|
||||
|
||||
calendar plugin
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
plugindev build
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Upload the .hmap file through the Plugin Manager API.
|
||||
7
example/calendar/go.mod
Normal file
7
example/calendar/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module calendar
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone
|
||||
11
example/calendar/plg.json
Normal file
11
example/calendar/plg.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "calendar",
|
||||
"name_zh": "日历",
|
||||
"name_en": "Calendar",
|
||||
"version": "1.0.0",
|
||||
"description": "日历事件管理,支持提醒和重复事件",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": ["calendar", "event", "reminder", "schedule"],
|
||||
"targets": "linux/amd64"
|
||||
}
|
||||
1140
example/calendar/plugin.go
Normal file
1140
example/calendar/plugin.go
Normal file
File diff suppressed because it is too large
Load Diff
7
example/music/go.mod
Normal file
7
example/music/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module music
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.7.1
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone
|
||||
11
example/music/plg.json
Normal file
11
example/music/plg.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "music",
|
||||
"name_zh": "音乐搜索",
|
||||
"name_en": "Music Search",
|
||||
"version": "0.1.0",
|
||||
"description": "音乐搜索插件,支持搜索歌曲和查看歌词(基于网易云音乐)",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": ["music", "song", "lyrics", "网易云"],
|
||||
"targets": "linux/amd64"
|
||||
}
|
||||
320
example/music/plugin.go
Normal file
320
example/music/plugin.go
Normal 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")
|
||||
}
|
||||
@ -808,7 +808,7 @@ func (p *Plugin) handleGetMessage(args map[string]interface{}) (interface{}, err
|
||||
"group_id": found.GroupID,
|
||||
"nickname": found.Nickname,
|
||||
"message_type": found.MessageType,
|
||||
"time": time.Unix(found.Time, 0).Format("15:04:05"),
|
||||
"time": time.Unix(found.Time, 0).Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
if found.RawText != "" {
|
||||
result["raw_text"] = found.RawText
|
||||
@ -1103,7 +1103,7 @@ func (p *Plugin) handleGetHistory(args map[string]interface{}) (interface{}, err
|
||||
}
|
||||
ts := ""
|
||||
if t, ok := msg["time"].(float64); ok {
|
||||
ts = time.Unix(int64(t), 0).Format("15:04")
|
||||
ts = time.Unix(int64(t), 0).Format("2006-01-02 15:04")
|
||||
}
|
||||
line := msgText
|
||||
if sender != "" {
|
||||
|
||||
13
example/rss/README.md
Normal file
13
example/rss/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# rss
|
||||
|
||||
rss plugin
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
plugindev build
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Upload the .hmap file through the Plugin Manager API.
|
||||
14
example/rss/go.mod
Normal file
14
example/rss/go.mod
Normal file
@ -0,0 +1,14 @@
|
||||
module rss
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||
|
||||
require (
|
||||
github.com/mmcdole/gofeed v1.4.0 // indirect
|
||||
github.com/mmcdole/goxpp/v2 v2.0.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
)
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone
|
||||
8
example/rss/go.sum
Normal file
8
example/rss/go.sum
Normal file
@ -0,0 +1,8 @@
|
||||
github.com/mmcdole/gofeed v1.4.0 h1:+efDmI/yJXJgTfa8we5zg9GAKsU+2d7tnpt9QZwvjLQ=
|
||||
github.com/mmcdole/gofeed v1.4.0/go.mod h1:ngV5MTB7UJko6fH3/fG5AkB/ABUGK1ZTePF9iRhzu/c=
|
||||
github.com/mmcdole/goxpp/v2 v2.0.0 h1:HrSCflxerUEqZQNq3u7ldtmE/XkwnTx4Zpq2DW4i5rQ=
|
||||
github.com/mmcdole/goxpp/v2 v2.0.0/go.mod h1:CUduYMnO9JB6Z/uqDn9Ormk/r8E9BsLQxHPWDZ961Os=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
11
example/rss/plg.json
Normal file
11
example/rss/plg.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "rss",
|
||||
"name_zh": "RSS订阅",
|
||||
"name_en": "RSS",
|
||||
"version": "1.0.0",
|
||||
"description": "RSS/Atom 订阅监控插件,自动检测更新并推送通知",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": ["rss", "feed", "subscription", "monitor"],
|
||||
"targets": "linux/amd64"
|
||||
}
|
||||
457
example/rss/plugin.go
Normal file
457
example/rss/plugin.go
Normal file
@ -0,0 +1,457 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
"github.com/mmcdole/gofeed"
|
||||
)
|
||||
|
||||
type FeedSub struct {
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
AddedAt string `json:"added_at"`
|
||||
Interval int `json:"interval"`
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
sdk *sdk.PluginSDK
|
||||
client *http.Client
|
||||
fp *gofeed.Parser
|
||||
dataDir string
|
||||
mu sync.RWMutex
|
||||
feeds []FeedSub
|
||||
seenGUIDs map[string]bool
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
pollTicker *time.Ticker
|
||||
}
|
||||
|
||||
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 readCfg[T string | int64 | float64](s sdk.SettingsAPI, key string, fallback T) T {
|
||||
v, err := s.Get(key)
|
||||
if err == nil && v != nil {
|
||||
if sv, ok := v.(string); ok && sv != "" {
|
||||
switch any(fallback).(type) {
|
||||
case string:
|
||||
return any(sv).(T)
|
||||
case int64:
|
||||
if n, err := strconv.ParseInt(sv, 10, 64); err == nil {
|
||||
return any(n).(T)
|
||||
}
|
||||
case float64:
|
||||
if n, err := strconv.ParseFloat(sv, 64); err == nil {
|
||||
return any(n).(T)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v2, err2 := s.GetCore("plugin." + "rss" + "." + key)
|
||||
if err2 == nil && v2 != nil {
|
||||
if sv, ok := v2.(string); ok && sv != "" {
|
||||
switch any(fallback).(type) {
|
||||
case string:
|
||||
return any(sv).(T)
|
||||
case int64:
|
||||
if n, err := strconv.ParseInt(sv, 10, 64); err == nil {
|
||||
return any(n).(T)
|
||||
}
|
||||
case float64:
|
||||
if n, err := strconv.ParseFloat(sv, 64); err == nil {
|
||||
return any(n).(T)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func readArg[T string | int64 | float64](args map[string]interface{}, key string, fallback T) T {
|
||||
v, ok := args[key]
|
||||
if !ok || v == nil {
|
||||
return fallback
|
||||
}
|
||||
switch any(fallback).(type) {
|
||||
case string:
|
||||
if s, ok := v.(string); ok {
|
||||
return any(s).(T)
|
||||
}
|
||||
case int64:
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return any(int64(n)).(T)
|
||||
case int64:
|
||||
return any(n).(T)
|
||||
case string:
|
||||
if i, err := strconv.ParseInt(n, 10, 64); err == nil {
|
||||
return any(i).(T)
|
||||
}
|
||||
}
|
||||
case float64:
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return any(n).(T)
|
||||
case int64:
|
||||
return any(float64(n)).(T)
|
||||
case string:
|
||||
if f, err := strconv.ParseFloat(n, 64); err == nil {
|
||||
return any(f).(T)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.sdk = s
|
||||
p.client = &http.Client{Timeout: 30 * time.Second}
|
||||
p.fp = gofeed.NewParser()
|
||||
p.stopCh = make(chan struct{})
|
||||
p.seenGUIDs = make(map[string]bool)
|
||||
p.feeds = []FeedSub{}
|
||||
|
||||
dataHome := os.Getenv("HOME")
|
||||
if dataHome == "" {
|
||||
dataHome = "/tmp"
|
||||
}
|
||||
p.dataDir = filepath.Join(dataHome, ".homeagent", "rss")
|
||||
os.MkdirAll(p.dataDir, 0755)
|
||||
p.loadData()
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin.rss.poll_interval", Default: "30", Type: "string",
|
||||
DisplayName: "Poll Interval", Description: "Default polling interval in minutes (default: 30)",
|
||||
Category: "rss",
|
||||
})
|
||||
|
||||
tp := p.name + "_"
|
||||
s.RegisterTool(tp+"subscribe", sdk.ToolDef{
|
||||
Name: tp + "subscribe", Description: "Subscribe to an RSS/Atom feed URL",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"url": map[string]interface{}{"type": "string", "description": "Feed URL"},
|
||||
"interval": map[string]interface{}{"type": "integer", "description": "Poll interval in minutes (default: 30, minimum: 5)"},
|
||||
},
|
||||
"required": []string{"url"},
|
||||
},
|
||||
}, p.handleSubscribe)
|
||||
|
||||
s.RegisterTool(tp+"unsubscribe", sdk.ToolDef{
|
||||
Name: tp + "unsubscribe", Description: "Unsubscribe from a feed",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"url": map[string]interface{}{"type": "string", "description": "Feed URL to unsubscribe"},
|
||||
},
|
||||
"required": []string{"url"},
|
||||
},
|
||||
}, p.handleUnsubscribe)
|
||||
|
||||
s.RegisterTool(tp+"list", sdk.ToolDef{
|
||||
Name: tp + "list", Description: "List all subscribed feeds",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
}, p.handleList)
|
||||
|
||||
s.RegisterTool(tp+"check_now", sdk.ToolDef{
|
||||
Name: tp + "check_now", Description: "Manually check all feeds for new articles now",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
}, p.handleCheckNow)
|
||||
|
||||
pollMin := int(readCfg(s.Settings(), "poll_interval", int64(30)))
|
||||
if pollMin < 5 {
|
||||
pollMin = 5
|
||||
}
|
||||
p.pollTicker = time.NewTicker(time.Duration(pollMin) * time.Minute)
|
||||
|
||||
p.wg.Add(1)
|
||||
go p.pollLoop()
|
||||
|
||||
fmt.Printf("[%s] started (%d feeds, poll every %dm)\n", p.name, len(p.feeds), pollMin)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
close(p.stopCh)
|
||||
p.pollTicker.Stop()
|
||||
p.wg.Wait()
|
||||
p.saveData()
|
||||
fmt.Printf("[%s] stopped\n", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) pollLoop() {
|
||||
defer p.wg.Done()
|
||||
|
||||
p.checkAllFeeds()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.pollTicker.C:
|
||||
p.checkAllFeeds()
|
||||
case <-p.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) checkAllFeeds() {
|
||||
p.mu.RLock()
|
||||
feeds := make([]FeedSub, len(p.feeds))
|
||||
copy(feeds, p.feeds)
|
||||
p.mu.RUnlock()
|
||||
|
||||
for _, feed := range feeds {
|
||||
select {
|
||||
case <-p.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
p.checkFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) checkFeed(sub FeedSub) {
|
||||
parsed, err := p.fp.ParseURL(sub.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
title := parsed.Title
|
||||
if title == "" {
|
||||
title = sub.URL
|
||||
}
|
||||
|
||||
var newArticles []*gofeed.Item
|
||||
for _, item := range parsed.Items {
|
||||
guid := item.GUID
|
||||
if guid == "" {
|
||||
guid = item.Link
|
||||
}
|
||||
if guid == "" {
|
||||
continue
|
||||
}
|
||||
guid = sub.URL + "|" + guid
|
||||
p.mu.RLock()
|
||||
seen := p.seenGUIDs[guid]
|
||||
p.mu.RUnlock()
|
||||
if !seen {
|
||||
newArticles = append(newArticles, item)
|
||||
}
|
||||
}
|
||||
|
||||
if len(newArticles) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, fmt.Sprintf("📡 %s (%s) — %d 篇新文章:", title, sub.URL, len(newArticles)))
|
||||
for _, item := range newArticles {
|
||||
pubDate := ""
|
||||
if item.PublishedParsed != nil {
|
||||
pubDate = item.PublishedParsed.Format("01-02 15:04")
|
||||
}
|
||||
line := fmt.Sprintf(" • %s", item.Title)
|
||||
if pubDate != "" {
|
||||
line += fmt.Sprintf(" [%s]", pubDate)
|
||||
}
|
||||
if item.Link != "" {
|
||||
line += "\n " + item.Link
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
p.sdk.InjectInterruptText("rss", "rss", strings.Join(lines, "\n"))
|
||||
|
||||
p.mu.Lock()
|
||||
for _, item := range newArticles {
|
||||
guid := item.GUID
|
||||
if guid == "" {
|
||||
guid = item.Link
|
||||
}
|
||||
if guid == "" {
|
||||
continue
|
||||
}
|
||||
p.seenGUIDs[sub.URL+"|"+guid] = true
|
||||
}
|
||||
p.mu.Unlock()
|
||||
p.saveData()
|
||||
}
|
||||
|
||||
func (p *Plugin) handleSubscribe(args map[string]interface{}) (interface{}, error) {
|
||||
url := readArg(args, "url", "")
|
||||
if url == "" {
|
||||
return map[string]interface{}{"isError": true, "content": "URL is required"}, nil
|
||||
}
|
||||
|
||||
p.mu.RLock()
|
||||
for _, f := range p.feeds {
|
||||
if f.URL == url {
|
||||
p.mu.RUnlock()
|
||||
return map[string]interface{}{"isError": true, "content": "Already subscribed to: " + url}, nil
|
||||
}
|
||||
}
|
||||
p.mu.RUnlock()
|
||||
|
||||
interval := int(readArg(args, "interval", int64(30)))
|
||||
if interval < 5 {
|
||||
interval = 5
|
||||
}
|
||||
|
||||
parsed, err := p.fp.ParseURL(url)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"isError": true, "content": "Failed to parse feed: " + err.Error()}, nil
|
||||
}
|
||||
|
||||
feedTitle := parsed.Title
|
||||
if feedTitle == "" {
|
||||
feedTitle = url
|
||||
}
|
||||
|
||||
sub := FeedSub{
|
||||
URL: url,
|
||||
Title: feedTitle,
|
||||
AddedAt: time.Now().Format("2006-01-02 15:04"),
|
||||
Interval: interval,
|
||||
}
|
||||
|
||||
guidCount := 0
|
||||
for _, item := range parsed.Items {
|
||||
guid := item.GUID
|
||||
if guid == "" {
|
||||
guid = item.Link
|
||||
}
|
||||
if guid == "" {
|
||||
continue
|
||||
}
|
||||
p.seenGUIDs[url+"|"+guid] = true
|
||||
guidCount++
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
p.feeds = append(p.feeds, sub)
|
||||
p.mu.Unlock()
|
||||
p.saveData()
|
||||
|
||||
return map[string]interface{}{
|
||||
"content": fmt.Sprintf("Subscribed to: %s\nTitle: %s\nArticles found: %d\nPoll interval: %d min", url, feedTitle, guidCount, interval),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleUnsubscribe(args map[string]interface{}) (interface{}, error) {
|
||||
url := readArg(args, "url", "")
|
||||
if url == "" {
|
||||
return map[string]interface{}{"isError": true, "content": "URL is required"}, nil
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
found := false
|
||||
for i, f := range p.feeds {
|
||||
if f.URL == url {
|
||||
p.feeds = append(p.feeds[:i], p.feeds[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
p.mu.Unlock()
|
||||
return map[string]interface{}{"isError": true, "content": "Not subscribed to: " + url}, nil
|
||||
}
|
||||
|
||||
for guid := range p.seenGUIDs {
|
||||
if strings.HasPrefix(guid, url+"|") {
|
||||
delete(p.seenGUIDs, guid)
|
||||
}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
p.saveData()
|
||||
|
||||
return map[string]interface{}{"content": "Unsubscribed: " + url}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleList(args map[string]interface{}) (interface{}, error) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
if len(p.feeds) == 0 {
|
||||
return map[string]interface{}{"content": "No subscriptions. Use rss_subscribe to add one."}, nil
|
||||
}
|
||||
|
||||
sort.Slice(p.feeds, func(i, j int) bool {
|
||||
return p.feeds[i].Title < p.feeds[j].Title
|
||||
})
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, fmt.Sprintf("📡 Subscriptions (%d):", len(p.feeds)))
|
||||
for _, f := range p.feeds {
|
||||
lines = append(lines, fmt.Sprintf(" • %s\n %s (every %dm, added %s)", f.Title, f.URL, f.Interval, f.AddedAt))
|
||||
}
|
||||
|
||||
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleCheckNow(args map[string]interface{}) (interface{}, error) {
|
||||
go p.checkAllFeeds()
|
||||
return map[string]interface{}{"content": "Checking all feeds for updates..."}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) dataFile() string {
|
||||
return filepath.Join(p.dataDir, "feeds.json")
|
||||
}
|
||||
|
||||
func (p *Plugin) loadData() {
|
||||
b, err := os.ReadFile(p.dataFile())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var data struct {
|
||||
Feeds []FeedSub `json:"feeds"`
|
||||
SeenGUIDs map[string]bool `json:"seen"`
|
||||
}
|
||||
if json.Unmarshal(b, &data) != nil {
|
||||
return
|
||||
}
|
||||
if data.Feeds != nil {
|
||||
p.feeds = data.Feeds
|
||||
}
|
||||
if data.SeenGUIDs != nil {
|
||||
p.seenGUIDs = data.SeenGUIDs
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) saveData() {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
data := struct {
|
||||
Feeds []FeedSub `json:"feeds"`
|
||||
SeenGUIDs map[string]bool `json:"seen"`
|
||||
}{
|
||||
Feeds: p.feeds,
|
||||
SeenGUIDs: p.seenGUIDs,
|
||||
}
|
||||
b, _ := json.MarshalIndent(data, "", " ")
|
||||
os.WriteFile(p.dataFile(), b, 0644)
|
||||
}
|
||||
|
||||
|
||||
13
example/weather/README.md
Normal file
13
example/weather/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# weather
|
||||
|
||||
weather plugin
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
plugindev build
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Upload the .hmap file through the Plugin Manager API.
|
||||
7
example/weather/go.mod
Normal file
7
example/weather/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module weather
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => /tmp/opencode/sdk-clone
|
||||
11
example/weather/plg.json
Normal file
11
example/weather/plg.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "weather",
|
||||
"name_zh": "天气查询",
|
||||
"name_en": "Weather",
|
||||
"version": "1.0.0",
|
||||
"description": "天气查询插件(基于 wttr.in),支持实时天气和未来预报",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": ["weather", "forecast", "wttr"],
|
||||
"targets": "linux/amd64"
|
||||
}
|
||||
388
example/weather/plugin.go
Normal file
388
example/weather/plugin.go
Normal file
@ -0,0 +1,388 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
sdk *sdk.PluginSDK
|
||||
client *http.Client
|
||||
defaultLoc string
|
||||
}
|
||||
|
||||
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.client = &http.Client{Timeout: 15 * time.Second}
|
||||
|
||||
loc, err := s.Settings().Get("default_location")
|
||||
if err == nil && loc != nil {
|
||||
if v, ok := loc.(string); ok && v != "" {
|
||||
p.defaultLoc = v
|
||||
}
|
||||
}
|
||||
if p.defaultLoc == "" {
|
||||
v, err := s.Settings().GetCore("plugin.weather.default_location")
|
||||
if err == nil && v != nil {
|
||||
if vs, ok := v.(string); ok && vs != "" {
|
||||
p.defaultLoc = vs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataHome := os.Getenv("HOME")
|
||||
if dataHome == "" {
|
||||
dataHome = "/tmp"
|
||||
}
|
||||
dataDir := filepath.Join(dataHome, ".homeagent", "weather")
|
||||
os.MkdirAll(dataDir, 0755)
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin.weather.default_location", Default: "", Type: "string",
|
||||
DisplayName: "Default Location", Description: "Default city name for weather queries, e.g. Beijing",
|
||||
Category: "weather",
|
||||
})
|
||||
|
||||
tp := p.name + "_"
|
||||
s.RegisterTool(tp+"current", sdk.ToolDef{
|
||||
Name: tp + "current", Description: "Get current weather for a city",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"location": map[string]interface{}{"type": "string", "description": "City name (e.g. Beijing, Shanghai, London). Uses default if omitted."},
|
||||
"units": map[string]interface{}{"type": "string", "description": "Units: metric (celsius) or imperial (fahrenheit), default metric"},
|
||||
},
|
||||
},
|
||||
}, p.handleCurrent)
|
||||
|
||||
s.RegisterTool(tp+"forecast", sdk.ToolDef{
|
||||
Name: tp + "forecast", Description: "Get weather forecast for next several days",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"location": map[string]interface{}{"type": "string", "description": "City name. Uses default if omitted."},
|
||||
"days": map[string]interface{}{"type": "integer", "description": "Number of days (1-7), default 3"},
|
||||
"units": map[string]interface{}{"type": "string", "description": "Units: metric or imperial, default metric"},
|
||||
},
|
||||
},
|
||||
}, p.handleForecast)
|
||||
|
||||
s.RegisterTool(tp+"set_location", sdk.ToolDef{
|
||||
Name: tp + "set_location", Description: "Set default weather location",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"location": map[string]interface{}{"type": "string", "description": "City name to set as default"},
|
||||
},
|
||||
"required": []string{"location"},
|
||||
},
|
||||
}, p.handleSetLocation)
|
||||
|
||||
fmt.Printf("[%s] started\n", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
fmt.Printf("[%s] stopped\n", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
type wttrResp struct {
|
||||
CurrentCondition []struct {
|
||||
TempC string `json:"temp_C"`
|
||||
FeelsLikeC string `json:"FeelsLikeC"`
|
||||
Humidity string `json:"humidity"`
|
||||
WindspeedKmph string `json:"windspeedKmph"`
|
||||
Winddir16Point string `json:"winddir16Point"`
|
||||
Pressure string `json:"pressure"`
|
||||
Visibility string `json:"visibility"`
|
||||
WeatherDesc []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"weatherDesc"`
|
||||
LocalObsDateTime string `json:"localObsDateTime"`
|
||||
} `json:"current_condition"`
|
||||
NearestArea []struct {
|
||||
AreaName []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"areaName"`
|
||||
Country []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"country"`
|
||||
Region []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"region"`
|
||||
} `json:"nearest_area"`
|
||||
Weather []wttrDay `json:"weather"`
|
||||
}
|
||||
|
||||
type wttrDay struct {
|
||||
Date string `json:"date"`
|
||||
Astronomy []struct {
|
||||
Sunrise string `json:"sunrise"`
|
||||
Sunset string `json:"sunset"`
|
||||
} `json:"astronomy"`
|
||||
MaxtempC string `json:"maxtempC"`
|
||||
MintempC string `json:"mintempC"`
|
||||
Hourly []struct {
|
||||
TempC string `json:"tempC"`
|
||||
WeatherDesc []struct {
|
||||
Value string `json:"value"`
|
||||
} `json:"weatherDesc"`
|
||||
WindspeedKmph string `json:"windspeedKmph"`
|
||||
Winddir16Point string `json:"winddir16Point"`
|
||||
Humidity string `json:"humidity"`
|
||||
FeelsLikeC string `json:"FeelsLikeC"`
|
||||
PrecipMM string `json:"precipMM"`
|
||||
Visibility string `json:"visibility"`
|
||||
} `json:"hourly"`
|
||||
}
|
||||
|
||||
func (p *Plugin) getLoc(args map[string]interface{}) string {
|
||||
if v, ok := args["location"].(string); ok && v != "" {
|
||||
return v
|
||||
}
|
||||
return p.defaultLoc
|
||||
}
|
||||
|
||||
func (p *Plugin) getUnits(args map[string]interface{}) string {
|
||||
if v, ok := args["units"].(string); ok && (v == "imperial" || v == "metric") {
|
||||
return v
|
||||
}
|
||||
return "metric"
|
||||
}
|
||||
|
||||
func (p *Plugin) fetchWttr(location string) (*wttrResp, error) {
|
||||
url := fmt.Sprintf("https://wttr.in/%s?format=j1", strings.ReplaceAll(location, " ", "%20"))
|
||||
resp, err := p.client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var data wttrResp
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data.CurrentCondition) == 0 {
|
||||
return nil, fmt.Errorf("no weather data for: %s", location)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) displayName(data *wttrResp) string {
|
||||
if len(data.NearestArea) == 0 {
|
||||
return "Unknown"
|
||||
}
|
||||
area := data.NearestArea[0]
|
||||
name := ""
|
||||
if len(area.AreaName) > 0 {
|
||||
name = area.AreaName[0].Value
|
||||
}
|
||||
region := ""
|
||||
if len(area.Region) > 0 {
|
||||
region = area.Region[0].Value
|
||||
}
|
||||
country := ""
|
||||
if len(area.Country) > 0 {
|
||||
country = area.Country[0].Value
|
||||
}
|
||||
var parts []string
|
||||
if name != "" {
|
||||
parts = append(parts, name)
|
||||
}
|
||||
if region != "" && region != name {
|
||||
parts = append(parts, region)
|
||||
}
|
||||
if country != "" {
|
||||
parts = append(parts, country)
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func convertCtoF(c string) string {
|
||||
if v, err := strconv.ParseFloat(c, 64); err == nil {
|
||||
return fmt.Sprintf("%.0f", v*9/5+32)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (p *Plugin) handleCurrent(args map[string]interface{}) (interface{}, error) {
|
||||
location := p.getLoc(args)
|
||||
if location == "" {
|
||||
return map[string]interface{}{"isError": true, "content": "No location specified. Provide a city name or set default_location."}, nil
|
||||
}
|
||||
|
||||
units := p.getUnits(args)
|
||||
|
||||
data, err := p.fetchWttr(location)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"isError": true, "content": "Weather request failed: " + err.Error()}, nil
|
||||
}
|
||||
|
||||
cc := data.CurrentCondition[0]
|
||||
place := p.displayName(data)
|
||||
|
||||
desc := ""
|
||||
if len(cc.WeatherDesc) > 0 {
|
||||
desc = cc.WeatherDesc[0].Value
|
||||
}
|
||||
|
||||
unitStr := "°C"
|
||||
windUnit := "km/h"
|
||||
tempStr := cc.TempC
|
||||
feelsStr := cc.FeelsLikeC
|
||||
if units == "imperial" {
|
||||
unitStr = "°F"
|
||||
windUnit = "mph"
|
||||
tempStr = convertCtoF(tempStr)
|
||||
feelsStr = convertCtoF(feelsStr)
|
||||
}
|
||||
|
||||
obsTime := cc.LocalObsDateTime
|
||||
if len(obsTime) > 16 {
|
||||
obsTime = obsTime[:16]
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("🌤 %s — %s\n🌡 %s%s (体感 %s%s)\n💧 湿度 %s%% | 💨 风速 %s %s %s\n🕐 %s",
|
||||
place, desc,
|
||||
tempStr, unitStr, feelsStr, unitStr,
|
||||
cc.Humidity, cc.WindspeedKmph, windUnit, cc.Winddir16Point,
|
||||
obsTime)
|
||||
|
||||
return map[string]interface{}{
|
||||
"content": result,
|
||||
"location": place,
|
||||
"temp": cc.TempC,
|
||||
"feels_like": cc.FeelsLikeC,
|
||||
"humidity": cc.Humidity,
|
||||
"wind_speed": cc.WindspeedKmph,
|
||||
"weather": desc,
|
||||
"observed": obsTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleForecast(args map[string]interface{}) (interface{}, error) {
|
||||
location := p.getLoc(args)
|
||||
if location == "" {
|
||||
return map[string]interface{}{"isError": true, "content": "No location specified."}, nil
|
||||
}
|
||||
|
||||
days := 3
|
||||
if v, ok := args["days"].(float64); ok {
|
||||
d := int(v)
|
||||
if d >= 1 && d <= 7 {
|
||||
days = d
|
||||
}
|
||||
}
|
||||
|
||||
units := p.getUnits(args)
|
||||
|
||||
data, err := p.fetchWttr(location)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"isError": true, "content": "Forecast request failed: " + err.Error()}, nil
|
||||
}
|
||||
|
||||
place := p.displayName(data)
|
||||
|
||||
unitStr := "°C"
|
||||
if units == "imperial" {
|
||||
unitStr = "°F"
|
||||
}
|
||||
|
||||
dayCount := days
|
||||
if dayCount > len(data.Weather) {
|
||||
dayCount = len(data.Weather)
|
||||
}
|
||||
daysData := data.Weather[:dayCount]
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, fmt.Sprintf("📅 %d日天气预报 — %s", days, place))
|
||||
for _, day := range daysData {
|
||||
t, err := time.Parse("2006-01-02", day.Date)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
weekday := t.Weekday().String()[:3]
|
||||
|
||||
maxT := day.MaxtempC
|
||||
minT := day.MintempC
|
||||
desc := ""
|
||||
precip := ""
|
||||
|
||||
if len(day.Hourly) > 0 {
|
||||
mid := len(day.Hourly) / 2
|
||||
if len(day.Hourly[mid].WeatherDesc) > 0 {
|
||||
desc = day.Hourly[mid].WeatherDesc[0].Value
|
||||
}
|
||||
totalPrecip := 0.0
|
||||
for _, h := range day.Hourly {
|
||||
if pv, err := strconv.ParseFloat(h.PrecipMM, 64); err == nil {
|
||||
totalPrecip += pv
|
||||
}
|
||||
}
|
||||
if totalPrecip > 0 {
|
||||
precip = fmt.Sprintf(" 🌧%.1fmm", totalPrecip)
|
||||
}
|
||||
}
|
||||
|
||||
if units == "imperial" {
|
||||
maxT = convertCtoF(maxT)
|
||||
minT = convertCtoF(minT)
|
||||
}
|
||||
|
||||
sunrise, sunset := "", ""
|
||||
if len(day.Astronomy) > 0 {
|
||||
sunrise = day.Astronomy[0].Sunrise
|
||||
sunset = day.Astronomy[0].Sunset
|
||||
}
|
||||
|
||||
line := fmt.Sprintf(" %s %s/%s — %s~%s%s %s", weekday, day.Date[5:], day.Date[8:], minT, maxT, unitStr, desc)
|
||||
if precip != "" {
|
||||
line += precip
|
||||
}
|
||||
if sunrise != "" && sunset != "" {
|
||||
line += fmt.Sprintf(" 🌅%s 🌇%s", sunrise, sunset)
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
cc := data.CurrentCondition[0]
|
||||
nowDesc := ""
|
||||
if len(cc.WeatherDesc) > 0 {
|
||||
nowDesc = cc.WeatherDesc[0].Value
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("\n当前:%s %s°C", nowDesc, cc.TempC))
|
||||
|
||||
return map[string]interface{}{
|
||||
"content": strings.Join(lines, "\n"),
|
||||
"location": place,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleSetLocation(args map[string]interface{}) (interface{}, error) {
|
||||
loc, _ := args["location"].(string)
|
||||
if loc == "" {
|
||||
return map[string]interface{}{"isError": true, "content": "Location is required"}, nil
|
||||
}
|
||||
|
||||
p.sdk.Settings().SetCore("plugin.weather.default_location", loc)
|
||||
p.defaultLoc = loc
|
||||
return map[string]interface{}{"content": fmt.Sprintf("Default location set to: %s", loc)}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user