Files
homeagent-sdk/example/calendar/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

1141 lines
31 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"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
const (
RepeatNone = "none"
RepeatDaily = "daily"
RepeatWeekday = "weekday"
RepeatWeekly = "weekly"
RepeatBiweekly = "biweekly"
RepeatMonthly = "monthly"
RepeatYearly = "yearly"
RepeatLunarYearly = "lunar_yearly"
)
type CalendarEvent struct {
ID string `json:"id"`
Title string `json:"title"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time,omitempty"`
AllDay bool `json:"all_day,omitempty"`
Location string `json:"location,omitempty"`
Note string `json:"note,omitempty"`
Reminds []int `json:"reminds,omitempty"`
RemindAt []int64 `json:"remind_at,omitempty"`
Repeat string `json:"repeat,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Lunar bool `json:"lunar,omitempty"`
LunarMonth int `json:"lunar_month,omitempty"`
LunarDay int `json:"lunar_day,omitempty"`
}
type Plugin struct {
name string
sdk *sdk.PluginSDK
dataDir string
mu sync.RWMutex
events []CalendarEvent
nextEventID int
stopCh chan struct{}
wg sync.WaitGroup
remindTicker *time.Ticker
}
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name, stopCh: make(chan struct{})}, 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." + "calendar" + "." + 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
}
// --- Time Helpers ---
var shortWeekday = map[time.Weekday]string{
time.Monday: "一", time.Tuesday: "二", time.Wednesday: "三",
time.Thursday: "四", time.Friday: "五", time.Saturday: "六", time.Sunday: "日",
}
func parseEventTime(s string) (time.Time, bool, bool) {
t, err := time.ParseInLocation("2006-01-02 15:04", s, time.Local)
if err == nil {
return t, false, true
}
t, err = time.ParseInLocation("2006-01-02", s, time.Local)
if err == nil {
return t, true, true
}
return time.Time{}, false, false
}
// --- Lunar Calendar Engine ---
var lunarInfo = []int{
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0,
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06aa0, 0x1a6c4, 0x0aae0,
0x092e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a4d0, 0x0d150, 0x0f252,
0x0d520,
}
func daysInLunarYear(year int) int {
if year < 1900 || year > 2100 {
return 365
}
y := lunarInfo[year-1900]
sum := 0
for i := 0x8000; i > 0; i >>= 1 {
if y&i > 0 {
sum += 30
} else {
sum += 29
}
}
return sum
}
func leapMonth(year int) int {
if year < 1900 || year > 2100 {
return 0
}
return lunarInfo[year-1900] & 0xf
}
func leapDays(year int) int {
if year < 1900 || year > 2100 {
return 0
}
if leapMonth(year) == 0 {
return 0
}
if lunarInfo[year-1900]&0x10000 > 0 {
return 30
}
return 29
}
func monthDays(year, month int) int {
if year < 1900 || year > 2100 || month < 1 || month > 12 {
return 30
}
if lunarInfo[year-1900]&(0x10000>>month) > 0 {
return 30
}
return 29
}
var baseSolar = func() time.Time { t, _ := time.ParseInLocation("2006-01-02", "1900-01-31", time.Local); return t }()
func lunarToSolar(year, month, day int) (time.Time, bool) {
if year < 1900 || year > 2100 || month < 1 || month > 12 || day < 1 || day > 30 {
return time.Time{}, false
}
offset := 0
for y := 1900; y < year; y++ {
offset += daysInLunarYear(y)
}
lm := leapMonth(year)
for m := 1; m < month; m++ {
offset += monthDays(year, m)
if m == lm {
offset += leapDays(year)
}
}
offset += day - 1
solar := baseSolar.AddDate(0, 0, offset)
return solar, true
}
func nextLunarYearly(targetMonth, targetDay int, after time.Time) (time.Time, bool) {
afterYear := after.Year()
for y := afterYear; y <= afterYear+2; y++ {
t, ok := lunarToSolar(y, targetMonth, targetDay)
if !ok {
continue
}
if t.After(after) || t.Equal(after) {
return t, true
}
}
return time.Time{}, false
}
// --- Plugin Lifecycle ---
func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.sdk = s
dataHome := os.Getenv("HOME")
if dataHome == "" {
dataHome = "/tmp"
}
p.dataDir = filepath.Join(dataHome, ".homeagent", "calendar")
os.MkdirAll(p.dataDir, 0755)
p.loadEvents()
tp := p.name + "_"
s.RegisterTool(tp+"event_add", sdk.ToolDef{
Name: tp + "event_add", Description: "Add a calendar event. Time: YYYY-MM-DD HH:MM or YYYY-MM-DD for all-day.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"title": map[string]interface{}{"type": "string", "description": "Event title"},
"start_time": map[string]interface{}{"type": "string", "description": "Start time (YYYY-MM-DD HH:MM or YYYY-MM-DD)"},
"end_time": map[string]interface{}{"type": "string", "description": "End time (optional)"},
"location": map[string]interface{}{"type": "string", "description": "Location (optional)"},
"note": map[string]interface{}{"type": "string", "description": "Notes (optional)"},
"remind_before": map[string]interface{}{"type": "string", "description": "Reminder minutes before event. Multiple: comma-separated, e.g. '15,60,1440' for 15min + 1hr + 1day before. 0 or empty = no reminder."},
"repeat": map[string]interface{}{"type": "string", "description": "Repeat: none, daily, weekday, weekly, biweekly, monthly, yearly, lunar_yearly"},
"lunar": map[string]interface{}{"type": "boolean", "description": "Whether the date is lunar calendar. If true, repeat=lunar_yearly by default. Also set lunar_month and lunar_day."},
"lunar_month": map[string]interface{}{"type": "integer", "description": "Lunar month (1-12), required when lunar=true"},
"lunar_day": map[string]interface{}{"type": "integer", "description": "Lunar day (1-30), required when lunar=true"},
},
"required": []string{"title", "start_time"},
},
}, p.handleEventAdd)
s.RegisterTool(tp+"event_list", sdk.ToolDef{
Name: tp + "event_list", Description: "List upcoming events. Shows date, time, repeat pattern.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"days": map[string]interface{}{"type": "integer", "description": "Days ahead (default 7, max 365)"},
},
},
}, p.handleEventList)
s.RegisterTool(tp+"event_delete", sdk.ToolDef{
Name: tp + "event_delete", Description: "Delete an event by ID. Deletes this and all future recurrences.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"id": map[string]interface{}{"type": "string", "description": "Event ID"},
},
"required": []string{"id"},
},
}, p.handleEventDelete)
s.RegisterTool(tp+"event_update", sdk.ToolDef{
Name: tp + "event_update", Description: "Update an event. Only provided fields change. Resets reminder state.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"id": map[string]interface{}{"type": "string", "description": "Event ID"},
"title": map[string]interface{}{"type": "string", "description": "New title"},
"start_time": map[string]interface{}{"type": "string", "description": "New start time"},
"end_time": map[string]interface{}{"type": "string", "description": "New end time"},
"location": map[string]interface{}{"type": "string", "description": "New location"},
"note": map[string]interface{}{"type": "string", "description": "New notes"},
"remind_before": map[string]interface{}{"type": "string", "description": "New reminder minutes (comma-separated)"},
"repeat": map[string]interface{}{"type": "string", "description": "New repeat type"},
"lunar": map[string]interface{}{"type": "boolean", "description": "Whether lunar calendar"},
"lunar_month": map[string]interface{}{"type": "integer", "description": "Lunar month 1-12"},
"lunar_day": map[string]interface{}{"type": "integer", "description": "Lunar day 1-30"},
},
"required": []string{"id"},
},
}, p.handleEventUpdate)
s.RegisterTool(tp+"today", sdk.ToolDef{
Name: tp + "today", Description: "Show today's events with countdown.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
}, p.handleToday)
s.RegisterTool(tp+"week", sdk.ToolDef{
Name: tp + "week", Description: "Show this week's events grouped by day.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
}, p.handleWeek)
s.RegisterTool(tp+"month", sdk.ToolDef{
Name: tp + "month", Description: "Show a month calendar grid with event dots.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"year": map[string]interface{}{"type": "integer", "description": "Year (default: current)"},
"month": map[string]interface{}{"type": "integer", "description": "Month 1-12 (default: current)"},
},
},
}, p.handleMonth)
s.RegisterTool(tp+"search", sdk.ToolDef{
Name: tp + "search", Description: "Search events by keyword in title, location, or notes.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"keyword": map[string]interface{}{"type": "string", "description": "Search keyword"},
},
"required": []string{"keyword"},
},
}, p.handleSearch)
p.remindTicker = time.NewTicker(30 * time.Second)
p.wg.Add(1)
go p.remindLoop()
fmt.Printf("[%s] started (%d events)\n", p.name, len(p.events))
return nil
}
func (p *Plugin) Stop() error {
p.remindTicker.Stop()
close(p.stopCh)
p.wg.Wait()
p.saveEvents()
fmt.Printf("[%s] stopped\n", p.name)
return nil
}
// --- Reminder Loop ---
func (p *Plugin) remindLoop() {
defer p.wg.Done()
for {
select {
case <-p.remindTicker.C:
p.checkReminders()
case <-p.stopCh:
return
}
}
}
func (p *Plugin) checkReminders() {
now := time.Now()
p.mu.Lock()
defer p.mu.Unlock()
changed := false
for i := range p.events {
e := &p.events[i]
evtTime, allDay, ok := parseEventTime(e.StartTime)
if !ok {
continue
}
if allDay || evtTime.Before(now) {
continue
}
for ri, remindMin := range e.Reminds {
if remindMin <= 0 {
continue
}
if ri < len(e.RemindAt) && e.RemindAt[ri] > 0 {
continue
}
remindAt := evtTime.Add(-time.Duration(remindMin) * time.Minute)
if !now.After(remindAt) && !now.Equal(remindAt) {
continue
}
if len(e.RemindAt) <= ri {
e.RemindAt = append(e.RemindAt, make([]int64, ri+1-len(e.RemindAt))...)
}
e.RemindAt[ri] = remindAt.Unix()
changed = true
timeUntil := evtTime.Sub(now).Round(time.Minute)
msg := fmt.Sprintf("⏰ 提醒: %s (%s)", e.Title, e.StartTime)
if timeUntil > 0 {
msg += fmt.Sprintf(" (还有%s)", timeUntil)
}
if len(e.Reminds) > 1 {
msg += fmt.Sprintf(" [第%d次提醒]", ri+1)
}
if e.Location != "" {
msg += fmt.Sprintf("\n📍 %s", e.Location)
}
if e.Note != "" {
msg += fmt.Sprintf("\n📝 %s", e.Note)
}
go p.sdk.InjectInterruptText("calendar", "calendar", msg)
}
}
newEvents := []CalendarEvent{}
for i := range p.events {
e := &p.events[i]
evtTime, _, ok := parseEventTime(e.StartTime)
if !ok {
continue
}
if !now.After(evtTime) {
continue
}
if e.Repeat == "" || e.Repeat == RepeatNone {
continue
}
next := p.nextOccurrence(*e, evtTime)
if next != nil {
pid := e.ID
if e.ParentID != "" {
pid = e.ParentID
}
next.ParentID = pid
newEvents = append(newEvents, *next)
changed = true
}
}
if len(newEvents) > 0 {
p.events = append(p.events, newEvents...)
}
p.cleanupPastEvents()
if changed {
p.saveEventsLocked()
}
}
func (p *Plugin) nextOccurrence(e CalendarEvent, evtTime time.Time) *CalendarEvent {
var next time.Time
switch e.Repeat {
case RepeatDaily:
next = evtTime.AddDate(0, 0, 1)
case RepeatWeekday:
next = evtTime.AddDate(0, 0, 1)
for next.Weekday() == time.Saturday || next.Weekday() == time.Sunday {
next = next.AddDate(0, 0, 1)
}
case RepeatWeekly:
next = evtTime.AddDate(0, 0, 7)
case RepeatBiweekly:
next = evtTime.AddDate(0, 0, 14)
case RepeatMonthly:
next = evtTime.AddDate(0, 1, 0)
case RepeatYearly:
next = evtTime.AddDate(1, 0, 0)
case RepeatLunarYearly:
if e.LunarMonth > 0 && e.LunarDay > 0 {
t, ok := nextLunarYearly(e.LunarMonth, e.LunarDay, evtTime)
if ok {
next = t
} else {
return nil
}
} else {
return nil
}
default:
return nil
}
timeStr := next.Format("2006-01-02 15:04")
if e.AllDay {
timeStr = next.Format("2006-01-02")
}
reminds := make([]int, len(e.Reminds))
copy(reminds, e.Reminds)
return &CalendarEvent{
ID: fmt.Sprintf("evt_%d_%d", next.Unix(), p.nextEventID),
Title: e.Title,
StartTime: timeStr,
EndTime: e.EndTime,
AllDay: e.AllDay,
Location: e.Location,
Note: e.Note,
Reminds: reminds,
Repeat: e.Repeat,
ParentID: e.ParentID,
}
}
func (p *Plugin) cleanupPastEvents() {
now := time.Now()
keep := []CalendarEvent{}
for _, e := range p.events {
evtTime, _, ok := parseEventTime(e.StartTime)
if !ok {
continue
}
if !now.After(evtTime) {
keep = append(keep, e)
continue
}
if e.Repeat != "" && e.Repeat != RepeatNone {
keep = append(keep, e)
}
}
p.events = keep
}
// --- Persistence ---
func (p *Plugin) eventsFile() string {
return filepath.Join(p.dataDir, "events.json")
}
func (p *Plugin) loadEvents() {
p.mu.Lock()
defer p.mu.Unlock()
b, err := os.ReadFile(p.eventsFile())
if err != nil {
p.events = nil
p.nextEventID = 1
return
}
var data struct {
Events []CalendarEvent `json:"events"`
NextEventID int `json:"next_id"`
}
if json.Unmarshal(b, &data) != nil {
p.events = nil
p.nextEventID = 1
return
}
p.events = data.Events
p.nextEventID = data.NextEventID
if p.nextEventID < 1 {
p.nextEventID = 1
}
if p.events == nil {
p.events = []CalendarEvent{}
}
}
func (p *Plugin) saveEvents() {
p.mu.RLock()
defer p.mu.RUnlock()
p.saveEventsLocked()
}
func (p *Plugin) saveEventsLocked() {
data := struct {
Events []CalendarEvent `json:"events"`
NextEventID int `json:"next_id"`
}{
Events: p.events,
NextEventID: p.nextEventID,
}
b, _ := json.MarshalIndent(data, "", " ")
os.WriteFile(p.eventsFile(), b, 0644)
}
// --- Helper: parse remind_before ---
func parseReminds(s string) []int {
s = strings.TrimSpace(s)
if s == "" || s == "0" {
return nil
}
parts := strings.Split(s, ",")
vals := make([]int, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
v, err := strconv.Atoi(p)
if err != nil || v <= 0 {
continue
}
vals = append(vals, v)
}
sort.Ints(vals)
return vals
}
// --- Helper: format event duration ---
func formatTimeUntil(t time.Time) string {
now := time.Now()
if t.Before(now) {
return "已开始"
}
d := t.Sub(now)
if d < time.Hour {
m := int(d.Minutes())
return fmt.Sprintf("还有%d分钟", m)
}
if d < 24*time.Hour {
h := int(d.Hours())
m := int(d.Minutes()) % 60
if m > 0 {
return fmt.Sprintf("还有%d小时%d分", h, m)
}
return fmt.Sprintf("还有%d小时", h)
}
d2 := int(d.Hours() / 24)
return fmt.Sprintf("还有%d天", d2)
}
// --- Tool: event_add ---
func (p *Plugin) handleEventAdd(args map[string]interface{}) (interface{}, error) {
title := readArg(args, "title", "")
startTime := readArg(args, "start_time", "")
if title == "" || startTime == "" {
return map[string]interface{}{"isError": true, "content": "title and start_time are required"}, nil
}
parsedStart, allDay, ok := parseEventTime(startTime)
if !ok {
return map[string]interface{}{"isError": true, "content": "Invalid start_time. Use YYYY-MM-DD HH:MM or YYYY-MM-DD."}, nil
}
endTime := readArg(args, "end_time", "")
if endTime != "" {
if _, _, ok := parseEventTime(endTime); !ok {
return map[string]interface{}{"isError": true, "content": "Invalid end_time."}, nil
}
}
location := readArg(args, "location", "")
note := readArg(args, "note", "")
remindStr := readArg(args, "remind_before", "")
reminds := parseReminds(remindStr)
lunar := false
if v := readArg(args, "lunar", ""); v == "true" {
lunar = true
}
lunarMonth := int(readArg(args, "lunar_month", int64(0)))
lunarDay := int(readArg(args, "lunar_day", int64(0)))
repeat := readArg(args, "repeat", RepeatNone)
if lunar && repeat == RepeatNone {
repeat = RepeatLunarYearly
}
switch repeat {
case RepeatNone, RepeatDaily, RepeatWeekday, RepeatWeekly, RepeatBiweekly, RepeatMonthly, RepeatYearly, RepeatLunarYearly:
default:
repeat = RepeatNone
}
if lunar && (lunarMonth < 1 || lunarMonth > 12 || lunarDay < 1 || lunarDay > 30) {
return map[string]interface{}{"isError": true, "content": "lunar_month (1-12) and lunar_day (1-30) required when lunar=true"}, nil
}
event := CalendarEvent{
ID: fmt.Sprintf("evt_%d_%d", parsedStart.Unix(), p.nextEventID),
Title: title,
StartTime: startTime,
EndTime: endTime,
AllDay: allDay,
Location: location,
Note: note,
Reminds: reminds,
Repeat: repeat,
Lunar: lunar,
LunarMonth: lunarMonth,
LunarDay: lunarDay,
}
p.mu.Lock()
p.events = append(p.events, event)
p.nextEventID++
p.mu.Unlock()
p.saveEvents()
detail := fmt.Sprintf("Event added: %s (ID: %s)", title, event.ID)
if len(reminds) > 0 {
parts := make([]string, len(reminds))
for i, r := range reminds {
parts[i] = fmt.Sprintf("%dmin", r)
}
detail += fmt.Sprintf(" | 提醒: %s", strings.Join(parts, ", "))
}
if repeat != RepeatNone {
detail += " | 重复: " + repeat
}
return map[string]interface{}{"content": detail}, nil
}
// --- Tool: event_list ---
func (p *Plugin) handleEventList(args map[string]interface{}) (interface{}, error) {
days := int(readArg(args, "days", int64(7)))
if days < 1 {
days = 1
}
if days > 365 {
days = 365
}
now := time.Now()
cutoff := now.AddDate(0, 0, days)
p.mu.RLock()
upcoming := make([]CalendarEvent, 0)
for _, e := range p.events {
evtTime, _, ok := parseEventTime(e.StartTime)
if !ok {
continue
}
if evtTime.Before(cutoff) && evtTime.After(now.Add(-24*time.Hour)) {
upcoming = append(upcoming, e)
}
}
p.mu.RUnlock()
sort.Slice(upcoming, func(i, j int) bool {
return upcoming[i].StartTime < upcoming[j].StartTime
})
if len(upcoming) == 0 {
return map[string]interface{}{"content": fmt.Sprintf("No events in the next %d days.", days)}, nil
}
var lines []string
lines = append(lines, fmt.Sprintf("📋 Events (%d):", len(upcoming)))
for _, e := range upcoming {
timeStr := e.StartTime
if e.EndTime != "" {
timeStr += " → " + e.EndTime
}
extra := ""
if e.Location != "" {
extra += " 📍" + e.Location
}
if len(e.Reminds) > 0 {
parts := make([]string, len(e.Reminds))
for i, r := range e.Reminds {
parts[i] = fmt.Sprintf("%d", r)
}
extra += " 🔔" + strings.Join(parts, ",")
}
if e.Lunar {
extra += fmt.Sprintf(" 🌙%d-%d", e.LunarMonth, e.LunarDay)
}
if e.Repeat != "" && e.Repeat != RepeatNone {
extra += " 🔄" + e.Repeat
}
if e.Note != "" {
extra += " 📝" + e.Note
}
lines = append(lines, fmt.Sprintf(" [%s] %s%s", timeStr, e.Title, extra))
}
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
}
// --- Tool: event_delete ---
func (p *Plugin) handleEventDelete(args map[string]interface{}) (interface{}, error) {
id := readArg(args, "id", "")
if id == "" {
return map[string]interface{}{"isError": true, "content": "Event ID is required"}, nil
}
p.mu.Lock()
defer p.mu.Unlock()
found := false
remaining := []CalendarEvent{}
for _, e := range p.events {
if e.ID == id {
found = true
continue
}
pid := e.ParentID
if pid == "" {
pid = e.ID
}
if pid == id {
continue
}
remaining = append(remaining, e)
}
if !found {
return map[string]interface{}{"isError": true, "content": "Event not found: " + id}, nil
}
p.events = remaining
p.saveEventsLocked()
return map[string]interface{}{"content": "Deleted event and all recurrences: " + id}, nil
}
// --- Tool: event_update ---
func (p *Plugin) handleEventUpdate(args map[string]interface{}) (interface{}, error) {
id := readArg(args, "id", "")
if id == "" {
return map[string]interface{}{"isError": true, "content": "Event ID is required"}, nil
}
p.mu.Lock()
defer p.mu.Unlock()
for i := range p.events {
if p.events[i].ID != id {
continue
}
e := &p.events[i]
if v := readArg(args, "title", ""); v != "" {
e.Title = v
}
if v := readArg(args, "start_time", ""); v != "" {
if _, allDay, ok := parseEventTime(v); ok {
e.StartTime = v
e.AllDay = allDay
}
}
if v := readArg(args, "end_time", ""); v != "" {
if _, _, ok := parseEventTime(v); ok {
e.EndTime = v
}
}
if v := readArg(args, "location", ""); v != "" {
e.Location = v
}
if v := readArg(args, "note", ""); v != "" {
e.Note = v
}
if v := readArg(args, "remind_before", ""); v != "" {
e.Reminds = parseReminds(v)
}
if v := readArg(args, "repeat", ""); v != "" {
switch v {
case RepeatNone, RepeatDaily, RepeatWeekday, RepeatWeekly, RepeatBiweekly, RepeatMonthly, RepeatYearly, RepeatLunarYearly:
e.Repeat = v
}
}
if v := readArg(args, "lunar", ""); v == "true" {
e.Lunar = true
} else if v == "false" {
e.Lunar = false
}
if v := readArg(args, "lunar_month", int64(0)); v > 0 {
e.LunarMonth = int(v)
}
if v := readArg(args, "lunar_day", int64(0)); v > 0 {
e.LunarDay = int(v)
}
e.RemindAt = nil
p.saveEventsLocked()
return map[string]interface{}{"content": "Event updated: " + e.Title}, nil
}
return map[string]interface{}{"isError": true, "content": "Event not found: " + id}, nil
}
// --- Tool: today ---
func (p *Plugin) handleToday(args map[string]interface{}) (interface{}, error) {
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
todayEnd := todayStart.AddDate(0, 0, 1)
p.mu.RLock()
events := make([]CalendarEvent, 0)
for _, e := range p.events {
evtTime, _, ok := parseEventTime(e.StartTime)
if !ok {
continue
}
if evtTime.After(todayStart.Add(-time.Hour)) && evtTime.Before(todayEnd) {
events = append(events, e)
}
}
p.mu.RUnlock()
sort.Slice(events, func(i, j int) bool {
return events[i].StartTime < events[j].StartTime
})
dateStr := now.Format("2006-01-02")
weekday := shortWeekday[now.Weekday()]
lines := []string{fmt.Sprintf("📅 %s 周%s — 今天", dateStr, weekday)}
if len(events) == 0 {
lines = append(lines, " 今天没有事件")
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
}
for _, e := range events {
evtTime, _, _ := parseEventTime(e.StartTime)
timeStr := e.StartTime
if now.Format("2006-01-02") == evtTime.Format("2006-01-02") {
timeStr = evtTime.Format("15:04")
}
countdown := formatTimeUntil(evtTime)
detail := fmt.Sprintf(" %s — %s (%s)", timeStr, e.Title, countdown)
if e.AllDay {
detail = fmt.Sprintf(" 🌙 %s (全天)", e.Title)
}
if e.Location != "" {
detail += " 📍" + e.Location
}
lines = append(lines, detail)
}
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
}
// --- Tool: week ---
func (p *Plugin) handleWeek(args map[string]interface{}) (interface{}, error) {
now := time.Now()
weekStart := now.AddDate(0, 0, -int(now.Weekday()-time.Monday))
if now.Weekday() == time.Sunday {
weekStart = now.AddDate(0, 0, -6)
}
weekEnd := weekStart.AddDate(0, 0, 7)
p.mu.RLock()
dayEvents := make(map[string][]CalendarEvent)
for _, e := range p.events {
evtTime, _, ok := parseEventTime(e.StartTime)
if !ok {
continue
}
if evtTime.After(weekStart.Add(-time.Hour)) && evtTime.Before(weekEnd) {
dayKey := evtTime.Format("2006-01-02")
dayEvents[dayKey] = append(dayEvents[dayKey], e)
}
}
p.mu.RUnlock()
for k := range dayEvents {
sort.Slice(dayEvents[k], func(i, j int) bool {
return dayEvents[k][i].StartTime < dayEvents[k][j].StartTime
})
}
lines := []string{fmt.Sprintf("📅 %s %s", weekStart.Format("01-02"), weekEnd.AddDate(0, 0, -1).Format("01-02"))}
eventCount := 0
for i := 0; i < 7; i++ {
d := weekStart.AddDate(0, 0, i)
dayKey := d.Format("2006-01-02")
wd := shortWeekday[d.Weekday()]
prefix := " "
if d.Format("2006-01-02") == now.Format("2006-01-02") {
prefix = "▶"
}
line := fmt.Sprintf("%s %s %s", prefix, d.Format("01-02"), wd)
if evts, ok := dayEvents[dayKey]; ok && len(evts) > 0 {
titles := make([]string, len(evts))
for i, e := range evts {
timeStr := e.StartTime
if !e.AllDay {
timeStr = parseTimeShort(e.StartTime)
} else {
timeStr = "全天"
}
titles[i] = fmt.Sprintf("%s %s", timeStr, e.Title)
eventCount++
}
line += " " + strings.Join(titles, ", ")
}
lines = append(lines, line)
}
if eventCount == 0 {
lines = append(lines, " 本周没有事件")
}
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
}
func parseTimeShort(s string) string {
t, _, ok := parseEventTime(s)
if !ok {
return s
}
return t.Format("15:04")
}
// --- Tool: month ---
func (p *Plugin) handleMonth(args map[string]interface{}) (interface{}, error) {
now := time.Now()
year := int(readArg(args, "year", int64(now.Year())))
month := int(readArg(args, "month", int64(now.Month())))
if month < 1 || month > 12 {
month = int(now.Month())
}
firstDay := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, now.Location())
lastDay := firstDay.AddDate(0, 1, -1)
daysInMonth := lastDay.Day()
startWeekday := int(firstDay.Weekday())
if startWeekday == 0 {
startWeekday = 7
}
p.mu.RLock()
daySet := make(map[int]bool)
for _, e := range p.events {
evtTime, _, ok := parseEventTime(e.StartTime)
if !ok {
continue
}
if evtTime.Year() == year && evtTime.Month() == time.Month(month) {
daySet[evtTime.Day()] = true
}
}
p.mu.RUnlock()
monthName := firstDay.Format("January")
lines := []string{fmt.Sprintf("📅 %d年%d月 (%s)", year, month, monthName)}
lines = append(lines, " 一 二 三 四 五 六 日")
lines = append(lines, "")
row := " "
for i := 1; i < startWeekday; i++ {
row += " "
}
for d := 1; d <= daysInMonth; d++ {
mark := " "
if daySet[d] {
mark = "•"
}
row += fmt.Sprintf(" %2d%s", d, mark)
wd := startWeekday - 1 + d
if wd%7 == 0 || d == daysInMonth {
lines = append(lines, row)
row = " "
}
}
count := 0
for d := range daySet {
count++
_ = d
}
lines = append(lines, fmt.Sprintf("\n本月 %d 天有事件", count))
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
}
// --- Tool: search ---
func (p *Plugin) handleSearch(args map[string]interface{}) (interface{}, error) {
keyword := strings.ToLower(readArg(args, "keyword", ""))
if keyword == "" {
return map[string]interface{}{"isError": true, "content": "keyword is required"}, nil
}
p.mu.RLock()
results := make([]CalendarEvent, 0)
for _, e := range p.events {
if strings.Contains(strings.ToLower(e.Title), keyword) ||
strings.Contains(strings.ToLower(e.Location), keyword) ||
strings.Contains(strings.ToLower(e.Note), keyword) {
results = append(results, e)
}
}
p.mu.RUnlock()
sort.Slice(results, func(i, j int) bool {
return results[i].StartTime < results[j].StartTime
})
if len(results) == 0 {
return map[string]interface{}{"content": fmt.Sprintf("No events match: %s", keyword)}, nil
}
var lines []string
lines = append(lines, fmt.Sprintf("🔍 Found %d events for \"%s\":", len(results), keyword))
for _, e := range results {
lines = append(lines, fmt.Sprintf(" [%s] %s (ID: %s)", e.StartTime, e.Title, e.ID))
}
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
}