Files
homeagent-sdk/example/weather/plugin.go
root 1a3e72e899 fix: use short config keys in RegisterDef, remove generics helpers
- qq, a2a, bili, files, rss: RegisterDef keys changed from
  namespaced (e.g. "plugin.qq.listen") to short flat keys ("listen")
- a2a, bili: Get() calls updated to match short keys
- rss: replaced readCfg generics with getSetting, added SetAutoRestart(true)
- files: already uses short key Get, only RegisterDef needed fixing
2026-07-21 13:20:13 +08:00

379 lines
9.7 KiB
Go
Raw 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"
"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}
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "default_location", Default: "", Type: "string",
DisplayName: "Default Location", Description: "Default city name for weather queries, e.g. Beijing",
Category: "weather",
})
if v, _ := s.Settings().Get("default_location"); v != nil {
if vs, ok := v.(string); ok {
p.defaultLoc = vs
}
}
dataHome := os.Getenv("HOME")
if dataHome == "" {
dataHome = "/tmp"
}
os.MkdirAll(filepath.Join(dataHome, ".homeagent", "weather"), 0755)
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().Set("default_location", loc)
p.defaultLoc = loc
return map[string]interface{}{"content": fmt.Sprintf("Default location set to: %s", loc)}, nil
}