Files

197 lines
8.3 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 (
"fmt"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// Book: create a calendar event AND (optionally) invite participants BY NAME,
// in one command, with automatic rollback if inviting fails.
//
// Steps: create the event (summary + start/end) → if --with is given, resolve
// each name to a unique userId and add them all as participants; if that add
// fails, delete the just-created event so we never leave a half-built event
// behind. Replaces `calendar event create` (copy eventId) →
// `contact +search-user` (copy each userId) → `calendar attendee add`.
//
// dws calendar +book --title "Q1 复盘会" \
// --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" \
// --with 张三,李四
var Book = shortcut.Shortcut{
Service: "calendar",
Command: "+book",
Product: "calendar",
Description: "创建日程,并可按姓名邀请参会人(自动解析 userId,失败自动回滚删除日程)",
Intent: "当你想快速排一个会/日程、并顺手把几位同事按姓名拉进来时使用;" +
"内部先建日程拿到 eventId,再把每个姓名解析成唯一 userId 批量加为参会人。" +
"如果加参会人失败,会自动删除刚建好的日程回滚,避免留下一个没人的空日程。" +
"会真实创建日程并发出参会邀请。",
Risk: shortcut.RiskWrite,
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "user_required", Idempotency: "unknown",
},
Contract: corecmd.ContractDecl{
Identity: contract.ToolIdentitySpec{
ProductID: "calendar",
Name: "shortcut_book",
CanonicalPath: "calendar.shortcut_book",
CLIPath: "calendar +book",
PrimaryCLIPath: "calendar +book",
},
Description: "创建日程,并可按姓名邀请参会人(自动解析 userId,失败自动回滚删除日程)",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
},
Selection: contract.SelectionSpec{
AgentSummary: "创建日程,并可按姓名邀请参会人(自动解析 userId,失败自动回滚删除日程)",
UseWhen: []string{"当你想快速排一个会/日程、并顺手把几位同事按姓名拉进来时使用;内部先建日程拿到 eventId,再把每个姓名解析成唯一 userId 批量加为参会人。如果加参会人失败,会自动删除刚建好的日程回滚,避免留下一个没人的空日程。会真实创建日程并发出参会邀请。"},
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{
"dws calendar +book --title \"周会\" --start \"2026-03-10T14:00:00+08:00\" --end \"2026-03-10T15:00:00+08:00\"",
"dws calendar +book --title \"Q1 复盘会\" --start \"2026-03-10T14:00:00+08:00\" --end \"2026-03-10T15:00:00+08:00\" --with 张三,李四",
},
},
},
Flags: []shortcut.Flag{
{Name: "title", Type: shortcut.FlagString, Desc: "日程标题", Required: true},
{Name: "start", Type: shortcut.FlagString, Desc: "开始时间(ISO8601,如 2026-03-10T14:00:00+08:00)", Required: true},
{Name: "end", Type: shortcut.FlagString, Desc: "结束时间(ISO8601,如 2026-03-10T15:00:00+08:00)", Required: true},
{Name: "with", Type: shortcut.FlagString, Desc: "参会人姓名,逗号分隔(可选)"},
},
Tips: []string{
`dws calendar +book --title "周会" --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00"`,
`dws calendar +book --title "Q1 复盘会" --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" --with 张三,李四`,
},
Execute: func(rt *shortcut.RuntimeContext) error {
// create_calendar_event params copied verbatim from the helper's create
// call site: summary + startDateTime/endDateTime (ISO strings, not millis).
createArgs := map[string]any{
"summary": rt.Str("title"),
"startDateTime": rt.Str("start"),
"endDateTime": rt.Str("end"),
}
// Fast path: no participants → create and print in one step.
if !rt.Changed("with") || strings.TrimSpace(rt.Str("with")) == "" {
return rt.CallMCP("create_calendar_event", createArgs)
}
// Step 1 — resolve every participant name to a unique userId BEFORE
// creating the event, so an unknown/ambiguous name fails cheaply without
// leaving a dangling event behind.
var userIDs []string
for _, name := range strings.Split(rt.Str("with"), ",") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
user, err := resolveUser(rt, name)
if err != nil {
return err
}
userIDs = append(userIDs, user.userID)
}
if len(userIDs) == 0 {
return apperrors.NewValidation("--with 需要至少一个有效的参会人姓名")
}
// Under --dry-run we resolved names (reads) to validate them, but must not
// create the event or send invites. Preview what would happen and return.
if rt.DryRun() {
return rt.Output(map[string]any{
"dryRun": true,
"wouldCreate": createArgs,
"wouldInvite": userIDs,
})
}
// Step 2 — create the event and pull the eventId from the response.
created, err := rt.CallMCPWriteData("calendar", "create_calendar_event", createArgs)
if err != nil {
return err
}
eventID := shortcutExtractEventID(created)
if eventID == "" {
return apperrors.NewValidation("日程创建成功但无法从返回结果中解析 eventId,已跳过邀请参会人")
}
// Step 3 — add all participants in one batch. attendeesToAdd copied
// verbatim from the helper's `attendee add` call site.
if _, err := rt.CallMCPWriteData("calendar", "add_calendar_participant", map[string]any{
"eventId": eventID,
"attendeesToAdd": userIDs,
}); err != nil {
// Rollback: delete the just-created event so we never leave a
// half-built event without its intended attendees.
_, delErr := rt.CallMCPWriteData("calendar", "delete_calendar_event", map[string]any{
"eventId": eventID,
})
if delErr != nil {
return apperrors.NewValidation(fmt.Sprintf(
"添加参会人失败:%v;且回滚删除日程 %s 也失败:%v(请手动删除该日程)",
err, eventID, delErr))
}
return apperrors.NewValidation(fmt.Sprintf(
"添加参会人失败:%v;已回滚删除日程 %s", err, eventID))
}
// Step 4 — success: print the final event detail (eventId + attendees).
return rt.CallMCP("get_calendar_detail", map[string]any{"eventId": eventID})
},
}
// shortcutExtractEventID digs the eventId out of a create_calendar_event
// response, tolerating the common shapes ({eventId}, {id}, {result:{...}},
// {event:{...}}) since the exact envelope is not guaranteed.
func shortcutExtractEventID(data map[string]any) string {
if data == nil {
return ""
}
if id := shortcutPickID(data); id != "" {
return id
}
for _, key := range []string{"result", "event", "data"} {
if nested, ok := data[key].(map[string]any); ok {
if id := shortcutPickID(nested); id != "" {
return id
}
}
}
return ""
}
func shortcutPickID(m map[string]any) string {
for _, k := range []string{"eventId", "id"} {
if v, ok := m[k].(string); ok && strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
func init() {
shortcut.Register(Book)
}