Files
dingtalk-workspace-cli/internal/shortcut/smart/conflicts.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

153 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (
"sort"
"strings"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// Conflicts: detect time-overlapping events on my calendar for a day — the
// scheduling sanity check ("do I have any double-bookings?") that neither the
// 1:1 layer offers. dws-native value.
//
// It lists list_calendar_events for the target day (default today, or N days
// ahead via --in-days), then locally finds every pair of events whose [start,end)
// intervals overlap and reports them. Read-only.
//
// dws calendar +conflicts
// dws calendar +conflicts --in-days 1
var Conflicts = shortcut.Shortcut{
Service: "calendar",
Command: "+conflicts",
Product: "calendar",
Description: "检测我某天日程的时间冲突(重叠/双重预订,默认今天)",
Intent: "当你想快速知道『我今天(或某天)的日程有没有时间冲突、撞车的会议』时使用;" +
"内部自动算出目标日期的时间范围(默认今天,可用 --in-days 指定几天后),列出当天全部日程," +
"再在本地两两比对开始/结束时间,找出所有时间段重叠的日程对并报告。" +
"只读操作,不修改任何日程;没有冲突时明确告诉你「无冲突」。",
Risk: shortcut.RiskRead,
Flags: []shortcut.Flag{
{Name: "in-days", Type: shortcut.FlagInt, Desc: "几天后(可选,0=今天默认,1=明天…)", Required: false},
},
Tips: []string{
`dws calendar +conflicts`,
`dws calendar +conflicts --in-days 1`,
},
Execute: func(rt *shortcut.RuntimeContext) error {
// Target day window in local time.
dayStart, dayEnd := calendarDayRange(rt.Int("in-days"))
data, err := rt.CallMCPData("calendar", "list_calendar_events", map[string]any{
"startTime": dayStart.UnixMilli(),
"endTime": dayEnd.UnixMilli(),
"calendarId": "primary",
})
if err != nil {
return err
}
// Collect events that carry both a start and an end, keeping the parsed
// times for overlap comparison.
type ev struct {
title string
start time.Time
end time.Time
}
var evs []ev
for _, e := range shortcutNextEventList(data) {
start, ok := shortcutNextEventStart(e)
if !ok {
continue
}
end, ok := conflictsEndTime(e)
if !ok {
continue
}
title, _ := e["summary"].(string)
if strings.TrimSpace(title) == "" {
title = "(无标题)"
}
evs = append(evs, ev{title: title, start: start, end: end})
}
sort.Slice(evs, func(i, j int) bool { return evs[i].start.Before(evs[j].start) })
// Find every overlapping pair. Two intervals [s1,e1),[s2,e2) overlap iff
// s1 < e2 && s2 < e1.
conflicts := make([]map[string]any, 0)
for i := 0; i < len(evs); i++ {
for j := i + 1; j < len(evs); j++ {
if evs[i].start.Before(evs[j].end) && evs[j].start.Before(evs[i].end) {
conflicts = append(conflicts, map[string]any{
"a": conflictsLabel(evs[i].title, evs[i].start, evs[i].end),
"b": conflictsLabel(evs[j].title, evs[j].start, evs[j].end),
"overlapStart": conflictsMax(evs[i].start, evs[j].start).Format("15:04"),
"overlapEnd": conflictsMin(evs[i].end, evs[j].end).Format("15:04"),
})
}
}
}
return rt.Output(map[string]any{
"date": dayStart.Format("2006-01-02"),
"eventCount": len(evs),
"conflictCount": len(conflicts),
"hasConflict": len(conflicts) > 0,
"conflicts": conflicts,
})
},
}
// conflictsEndTime parses an event's end time from end.dateTime (RFC3339).
func conflictsEndTime(e map[string]any) (time.Time, bool) {
end, ok := e["end"].(map[string]any)
if !ok {
return time.Time{}, false
}
dt, ok := end["dateTime"].(string)
if !ok || dt == "" {
return time.Time{}, false
}
t, err := time.Parse(time.RFC3339, dt)
if err != nil {
return time.Time{}, false
}
return t, true
}
func conflictsLabel(title string, start, end time.Time) string {
return title + "(" + start.Format("15:04") + "-" + end.Format("15:04") + ")"
}
func conflictsMax(a, b time.Time) time.Time {
if a.After(b) {
return a
}
return b
}
func conflictsMin(a, b time.Time) time.Time {
if a.Before(b) {
return a
}
return b
}
func init() {
shortcut.Register(Conflicts)
}