Files
dingtalk-workspace-cli/internal/shortcut/smart/next_event.go
DennisandClaude Opus 4.8 4e4705c673 feat(shortcut): smart orchestration layer (68 shortcuts)
Multi-step / intelligent shortcuts under internal/shortcut/smart: name→ID
resolvers (user/base/table/dept/space), name-based actions (chat +dm/+broadcast/
+group-members, todo +assign, calendar +book with rollback/+free/+invite/
+suggest-time), time & self intelligence (calendar +today/+tomorrow/+week/
+next-event/+my-free and +conflicts/+free-slots scheduling intelligence),
convenience reads (contact +me, oa +pending/+done-approvals, todo +due-today/
+related-tasks, mail +recent-mail/+find-mail-user, attendance +this-month) and
aggregation (minutes +detail, aitable +record-share-links). Projections hardened
against real DingTalk responses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 10:04:10 +08:00

179 lines
5.9 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package smart
import (
"strings"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// NextEvent: show the single upcoming calendar event that starts soonest,
// looking at the next 7 days from now.
//
// Steps: list the current user's primary-calendar events over
// [now, now+7d] (list_calendar_events, times in epoch millis, params copied
// verbatim from the helper's `event list` call site) → defensively parse the
// event list → pick the one with the earliest start time that is still in the
// future → print a one-line summary. Replaces eyeballing a full
// `calendar event list` dump just to find "what's next". Read-only.
//
// dws calendar +next-event
var NextEvent = shortcut.Shortcut{
Service: "calendar",
Command: "+next-event",
Product: "calendar",
Description: "查看接下来最近的一个日程(默认扫描未来 7 天)",
Intent: "当你只想知道『我下一个日程是什么、什么时候开始』、而不想翻一整份日程列表时使用;" +
"内部以当前时间为起点、往后 7 天为范围,拉取主日历下的日程," +
"按开始时间升序挑出最近的那一个并打印摘要(标题、开始/结束时间、地点)。" +
"若这 7 天内没有任何日程,会明确提示『近 7 天无日程』。只读,不做任何修改。",
Risk: shortcut.RiskRead,
Flags: []shortcut.Flag{},
Tips: []string{`dws calendar +next-event`},
Execute: func(rt *shortcut.RuntimeContext) error {
// Window: [now, now+7d]. list_calendar_events expects epoch millis for
// startTime/endTime and calendarId (all copied from the helper's
// `event list` call site).
now := time.Now()
params := map[string]any{
"calendarId": "primary",
"startTime": now.UnixMilli(),
"endTime": now.Add(7 * 24 * time.Hour).UnixMilli(),
}
data, err := rt.CallMCPData("calendar", "list_calendar_events", params)
if err != nil {
return err
}
event := shortcutNextEventPick(data, now)
if event == nil {
return rt.Output(map[string]any{"event": nil, "message": "近 7 天无日程"})
}
// Emit a structured event via rt.Output so it honours
// --format/--jq/--fields (previously it printed a fixed text line and
// ignored the output flags, unlike +today/+week).
return rt.Output(map[string]any{"event": shortcutNextEventProject(event)})
},
}
// shortcutNextEventPick walks a list_calendar_events response, keeps only valid
// events (must carry an id) whose start time is at or after `now`, and returns
// the one that starts soonest. Field access is fully defensive because the
// response envelope and per-event shape are not guaranteed.
func shortcutNextEventPick(data map[string]any, now time.Time) map[string]any {
var best map[string]any
var bestStart time.Time
for _, e := range shortcutNextEventList(data) {
id, _ := e["id"].(string)
if strings.TrimSpace(id) == "" {
continue
}
start, ok := shortcutNextEventStart(e)
if !ok {
continue
}
if start.Before(now) {
continue
}
if best == nil || start.Before(bestStart) {
best = e
bestStart = start
}
}
return best
}
// shortcutNextEventList flattens the events slice out of the common response
// shapes ({result:{events:[...]}}, {events:[...]}, or a bare [...] under
// result/data).
func shortcutNextEventList(data map[string]any) []map[string]any {
if data == nil {
return nil
}
var raw []any
if result, ok := data["result"].(map[string]any); ok {
if ev, ok := result["events"].([]any); ok {
raw = ev
}
}
if raw == nil {
if ev, ok := data["events"].([]any); ok {
raw = ev
}
}
out := make([]map[string]any, 0, len(raw))
for _, item := range raw {
if m, ok := item.(map[string]any); ok {
out = append(out, m)
}
}
return out
}
// shortcutNextEventStart pulls an event's start time as a time.Time, tolerating
// either the nested {start:{dateTime: RFC3339}} shape or a flat string field.
func shortcutNextEventStart(event map[string]any) (time.Time, bool) {
if start, ok := event["start"].(map[string]any); ok {
if dt, ok := start["dateTime"].(string); ok && dt != "" {
if t, err := time.Parse(time.RFC3339, dt); err == nil {
return t, true
}
}
}
for _, k := range []string{"startTime", "startDateTime"} {
if dt, ok := event[k].(string); ok && dt != "" {
if t, err := time.Parse(time.RFC3339, dt); err == nil {
return t, true
}
}
}
return time.Time{}, false
}
// shortcutNextEventProject reshapes the chosen event into a structured
// {title,start,end,location,eventId} map, degrading gracefully when optional
// fields are missing. Mirrors the projection used by +today / +week.
func shortcutNextEventProject(event map[string]any) map[string]any {
title, _ := event["summary"].(string)
if strings.TrimSpace(title) == "" {
title = "(无标题)"
}
item := map[string]any{"title": title}
if id, ok := event["id"].(string); ok && strings.TrimSpace(id) != "" {
item["eventId"] = id
}
if start, ok := shortcutNextEventStart(event); ok {
item["start"] = start.Format("2006-01-02 15:04")
}
if end, ok := event["end"].(map[string]any); ok {
if dt, ok := end["dateTime"].(string); ok && dt != "" {
if t, err := time.Parse(time.RFC3339, dt); err == nil {
item["end"] = t.Format("2006-01-02 15:04")
}
}
}
if loc, ok := event["location"].(string); ok && strings.TrimSpace(loc) != "" {
item["location"] = loc
}
return item
}
func init() {
shortcut.Register(NextEvent)
}