Documentation
¶
Overview ¶
Package smart holds genuine multi-step / intelligent shortcuts — commands that orchestrate several MCP calls or resolve names to IDs, so they are NOT a 1:1 wrapper over a single tool. This is the "shortcut as a real capability" layer, distinct from the 1:1 ergonomic wrappers under the per-service packages.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ActionItems = shortcut.Shortcut{ Service: "minutes", Command: "+action-items", Product: "minutes", Description: "取我最新一条妙记里的待办事项", Intent: "当你刚开完会、只想立刻知道自己最近这条听记里系统帮你提取出了哪些待办事项," + "却不想先翻听记列表、复制 taskUuid、再手动去查待办时使用;" + "内部会先列出你创建的听记,自动挑出最新的一条,再拉取这条听记里提取的待办事项清单(含待办内容、参与人、待办时间等)。" + "这是只读操作,不会新增或修改任何待办;若你名下没有任何听记则提示「暂无妙记」。", Risk: shortcut.RiskRead, Tips: []string{ `dws minutes +action-items`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("minutes", "list_by_keyword_and_time_range", map[string]any{ "belongingConditionId": "created", "maxResults": float64(20), }) if err != nil { return err } taskUUID := latestMinutesTaskUUID(data) if taskUUID == "" { return apperrors.NewValidation("暂无妙记") } return rt.CallMCP("list_minutes_todos", map[string]any{ "taskUuid": taskUUID, }) }, }
ActionItems: fetch the extracted to-do items (待办事项) from MY most recent minutes (听记) in one step.
Steps:
- list my minutes via list_by_keyword_and_time_range (belongingConditionId = "created", maxResults = 20);
- pick the newest entry (largest create time, falling back to the first item since lists come back newest-first) and read its taskUuid;
- print that minute's extracted to-do items via list_minutes_todos.
If the list is empty it reports "暂无妙记" instead of failing obscurely.
dws minutes +action-items
var Approve = shortcut.Shortcut{ Service: "oa", Command: "+approve-by", Product: "oa", Description: "按关键词把我的一条待审批单据一键通过(自动定位实例与任务 ID)", Intent: "当你只记得某张待你审批单据的标题或单号关键词、想直接把它审批通过,却不想先翻待办列表、" + "复制审批实例 ID 再查任务 ID 时使用;内部先拉取你近三个月待处理的审批单,按标题/单号包含关键词匹配:" + "没匹配到会提示「没找到待审批单据」,匹配到多条会列出候选(标题+实例ID)让你写得更精确," + "唯一命中时先取出它的待审批任务 ID,再以「同意」提交。这会真实提交审批决策且不可撤回,请确认关键词足够精确。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "keyword", Type: shortcut.FlagString, Desc: "待审批单据的单号或标题关键词", Required: true}, {Name: "comment", Type: shortcut.FlagString, Desc: "审批意见(可选)", Required: false}, }, Tips: []string{ `dws oa +approve-by --keyword 报销`, `dws oa +approve-by --keyword 出差单 --comment "同意"`, }, Execute: func(rt *shortcut.RuntimeContext) error { keyword := strings.TrimSpace(rt.Str("keyword")) if keyword == "" { return apperrors.NewValidation("请用 --keyword 提供待审批单据的单号或标题关键词") } now := time.Now() listArgs := map[string]any{ "starTime": float64(now.AddDate(0, 0, -90).UnixMilli()), "endTime": float64(now.UnixMilli()), "query": keyword, } data, err := rt.CallMCPData("oa", "list_pending_approvals", listArgs) if err != nil { return err } matches := shortcutApproveMatch(data, keyword) switch { case len(matches) == 0: return apperrors.NewValidation(fmt.Sprintf( "没找到待审批单据:待我处理的审批里没有标题/单号包含 %q 的单据。", keyword)) case len(matches) > 1: return apperrors.NewValidation(fmt.Sprintf( "%q 匹配到 %d 条待审批单据,请用更精确的关键词,或用 `dws oa approval approve --instance-id --task-id` 指定:%s", keyword, len(matches), strings.Join(shortcutApproveLabels(matches), ";"))) } instanceID := matches[0].instanceID tasksData, err := rt.CallMCPData("oa", "list_pending_tasks", map[string]any{ "processInstanceId": instanceID, }) if err != nil { return err } taskID := shortcutApproveTaskID(tasksData) if taskID == "" { return apperrors.NewValidation(fmt.Sprintf( "没能拿到 %q 的待审批任务 ID,请用 `dws oa approval tasks --instance-id %s` 查看后手动 approve。", matches[0].title, instanceID)) } taskIDNum, err := strconv.ParseFloat(taskID, 64) if err != nil { return apperrors.NewValidation(fmt.Sprintf("任务 ID %q 不是合法数字,无法审批。", taskID)) } approveArgs := map[string]any{ "processInstanceId": instanceID, "taskId": taskIDNum, } if c := strings.TrimSpace(rt.Str("comment")); c != "" { approveArgs["remark"] = c } return rt.CallMCP("approve_processInstance", approveArgs) }, }
Approve: agree to ONE of MY pending OA approvals by matching a keyword in its title / order number, in a single command (钉钉原生编排能力).
Steps (all tool names + params copied verbatim from helpers/oa.go):
list my pending approvals via list_pending_approvals (starTime / endTime as float64 millis over a recent window, plus query=keyword for server-side narrowing — mirroring `dws oa approval list-pending`);
scan the returned instances and keep the ones whose title / order number contains --keyword: none → "没找到待审批单据", many → list candidates (标题 + processInstanceId) so the caller can be more specific, exactly one → take its processInstanceId;
resolve the pending taskId for that instance via list_pending_tasks (processInstanceId), then agree via approve_processInstance (processInstanceId + taskId as float64, optional remark) — mirroring `dws oa approval tasks` + `dws oa approval approve`.
dws oa +approve-by --keyword 报销 dws oa +approve-by --keyword 出差单 --comment "同意"
var Assign = shortcut.Shortcut{ Service: "todo", Command: "+assign", Product: "todo", Description: "按姓名给某人创建并指派一条待办(自动解析 userId)", Intent: "当你想把一件事指派给某位同事、但只知道对方姓名不想先查 userId 时使用;" + "内部先按姓名解析出唯一 userId,再创建待办并把 TA 设为执行人。会真实创建待办。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "to", Type: shortcut.FlagString, Desc: "执行人姓名/花名", Required: true}, {Name: "task", Type: shortcut.FlagString, Desc: "待办标题/内容", Required: true}, {Name: "due", Type: shortcut.FlagString, Desc: "截止时间(ISO8601,可选)"}, }, Tips: []string{`dws todo +assign --to 张三 --task "整理周报"`}, Execute: func(rt *shortcut.RuntimeContext) error { user, err := resolveUser(rt, rt.Str("to")) if err != nil { return err } vo := map[string]any{ "subject": rt.Str("task"), "executorIds": []string{user.userID}, } if rt.Changed("due") { ms, err := shortcutRemindParseMillis("due", rt.Str("due")) if err != nil { return err } vo["dueTime"] = ms } return rt.CallMCP("create_personal_todo", map[string]any{ "PersonalTodoCreateVO": vo, }) }, }
Assign: create a todo AND assign it to a person by NAME, in one command.
Steps: resolve the assignee name → userId → create a personal todo with that user as executor. Replaces `contact +search-user` (copy userId) → `todo +create --executors <id>`.
dws todo +assign --to 张三 --task "整理周报"
var AssignMulti = shortcut.Shortcut{ Service: "todo", Command: "+assign-multi", Product: "todo", Description: "把一条待办按姓名一次性指派给多个人(自动把每个姓名解析成 userId)", Intent: "当你想把同一条待办同时指派给好几个同事、但手上只有他们的姓名而不是 userId 时使用;" + "内部会把 --to 里的每个姓名逐个解析成唯一 userId,只要有任何一个姓名查不到或者重名有歧义," + "就把这些问题一次性汇总报错、并且完全不创建待办(不会建出只指派了一半人的残缺待办)。" + "全部姓名都解析成功后,才用这些 userId 一次性创建这条待办并指派给所有人。会真实创建一条新的待办。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "to", Type: shortcut.FlagStringSlice, Desc: "执行人姓名/花名,逗号分隔(如 张三,李四)", Required: true}, {Name: "task", Type: shortcut.FlagString, Desc: "待办标题", Required: true}, }, Tips: []string{ `dws todo +assign-multi --to "张三,李四" --task "周五前提交排期"`, }, Execute: func(rt *shortcut.RuntimeContext) error { task := strings.TrimSpace(rt.Str("task")) if task == "" { return apperrors.NewValidation("--task 不能为空(待办标题)") } // Normalize the --to name list, dropping blanks. var names []string for _, n := range rt.StrSlice("to") { if n = strings.TrimSpace(n); n != "" { names = append(names, n) } } if len(names) == 0 { return apperrors.NewValidation("--to 不能为空,请至少提供一个执行人姓名") } // Resolve every name up front. Collect all failures and abort before any // write, so we never create a todo assigned to only some of the people. var ( executorIDs []string resolved []string failures []string ) for _, name := range names { user, err := resolveUser(rt, name) if err != nil { failures = append(failures, fmt.Sprintf("%s: %s", name, err.Error())) continue } executorIDs = append(executorIDs, user.userID) resolved = append(resolved, fmt.Sprintf("%s(%s)", user.name, user.userID)) } if len(failures) > 0 { return apperrors.NewValidation(fmt.Sprintf( "以下执行人姓名无法唯一解析,已中止、未创建任何待办:\n%s", strings.Join(failures, "\n"))) } if len(executorIDs) == 0 { return apperrors.NewValidation("没有解析出任何有效的执行人 userId,已中止") } if err := rt.CallMCP("create_personal_todo", map[string]any{ "PersonalTodoCreateVO": map[string]any{ "subject": task, "executorIds": executorIDs, }, }); err != nil { return err } return rt.Output(map[string]any{ "subject": task, "executors": resolved, "count": len(executorIDs), }) }, }
AssignMulti: create ONE personal todo and assign it to SEVERAL people at once, addressing every executor by NAME instead of by userId.
Steps: split the --to name CSV, resolve each name to a unique userId (resolveUser, which never guesses — it errors on unknown/ambiguous names), collect ALL resolution errors first and abort if any name failed, so we never create a half-assigned todo. Then create the todo once via create_personal_todo with every resolved userId in executorIds. Replaces the manual dance of `contact user search` per name → copy each userId → `todo task create --executors id1,id2,... --title ...`.
tool name + params (create_personal_todo, PersonalTodoCreateVO.subject / .executorIds) are copied verbatim from the todo helper's `task create` call site.
dws todo +assign-multi --to "张三,李四,王五" --task "周五前提交排期"
var AtMe = shortcut.Shortcut{ Service: "chat", Command: "+at-me", Product: "chat", Description: "查最近 @我 的消息(自动算时间窗,投影发送人/时间/内容/会话)", Intent: "当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;" + "内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息," + "再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。" + "这是纯只读操作,只做搜索与本地投影,不会发送、撤回或标记任何消息。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "days", Type: shortcut.FlagInt, Desc: "回溯天数(可选,默认 7)", Default: "7", Required: false}, }, Tips: []string{ `dws chat +at-me`, `dws chat +at-me --days 3`, }, Execute: func(rt *shortcut.RuntimeContext) error { days := rt.Int("days") if days <= 0 { days = 7 } now := time.Now() startMs := now.AddDate(0, 0, -days).UnixMilli() endMs := now.UnixMilli() data, err := rt.CallMCPData("chat", "search_at_me_message", map[string]any{ "startTime": startMs, "endTime": endMs, "limit": 50, "cursor": "0", }) if err != nil { return err } items := atMeMessageItems(data) if len(items) == 0 { return rt.Output(data) } results := make([]map[string]any, 0, len(items)) for _, m := range items { results = append(results, map[string]any{ "sender": atMeSender(m), "time": atMeTime(m), "text": atMeText(m), "conversation": atMeConversation(m), }) } return rt.Output(map[string]any{"messages": results}) }, }
AtMe: pull the messages that recently @-mentioned ME across chats in one step.
Steps:
- compute the look-back window [now-Nd, now] in local time and express both bounds as epoch millis. N defaults to 7 days and is overridable via --days; mirroring `dws chat message list-mentions`, which feeds startTime/ endTime as epoch millis to search_at_me_message.
- call search_at_me_message on the chat server with startTime/endTime/limit/ cursor — the exact parameter names + first-page defaults (limit 50, cursor "0") used by helpers.chatMessageListMentionsCmd.
- defensively project each returned message down to {sender, time, text, conversation} (multiple candidate keys per field) and print via rt.Output so it honours --format/--jq/--fields. When the response carries no recognisable message list we fall back to printing the raw payload.
This replaces manually working out the millisecond time window and copying the list-mentions incantation. Read-only: it only searches and reshapes, never sends, recalls or marks anything.
dws chat +at-me dws chat +at-me --days 3
var Book = shortcut.Shortcut{ Service: "calendar", Command: "+book", Product: "calendar", Description: "创建日程,并可按姓名邀请参会人(自动解析 userId,失败自动回滚删除日程)", Intent: "当你想快速排一个会/日程、并顺手把几位同事按姓名拉进来时使用;" + "内部先建日程拿到 eventId,再把每个姓名解析成唯一 userId 批量加为参会人。" + "如果加参会人失败,会自动删除刚建好的日程回滚,避免留下一个没人的空日程。" + "会真实创建日程并发出参会邀请。", Risk: shortcut.RiskWrite, 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 { createArgs := map[string]any{ "summary": rt.Str("title"), "startDateTime": rt.Str("start"), "endDateTime": rt.Str("end"), } 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 需要至少一个有效的参会人姓名") } if rt.DryRun() { return rt.Output(map[string]any{ "dryRun": true, "wouldCreate": createArgs, "wouldInvite": userIDs, }) } created, err := rt.CallMCPWriteData("calendar", "create_calendar_event", createArgs) if err != nil { return err } eventID := shortcutExtractEventID(created) if eventID == "" { return apperrors.NewValidation("日程创建成功但无法从返回结果中解析 eventId,已跳过邀请参会人") } if _, err := rt.CallMCPWriteData("calendar", "add_calendar_participant", map[string]any{ "eventId": eventID, "attendeesToAdd": userIDs, }); err != nil { _, 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)) } return rt.CallMCP("get_calendar_detail", map[string]any{"eventId": eventID}) }, }
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 Broadcast = shortcut.Shortcut{ Service: "chat", Command: "+broadcast", Product: "chat", Description: "按姓名逐一给多个人群发同一条单聊消息(自动解析 userId、逐个发送)", Intent: "当你想把同一条通知一次性单聊发给多位同事、但只知道他们的姓名不想逐个查 userId 时使用;" + "内部把姓名列表逐个解析成唯一用户后,用 openDingTalkId 对每个人单独发一条单聊消息,并汇总成功/失败人数。" + "某个姓名匹配不到人或匹配到多人时,会跳过该人并在结尾报出,不影响其他人收到消息。会真实发出多条消息。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "to", Type: shortcut.FlagStringSlice, Desc: "收件人姓名/花名,逗号分隔的多个人", Required: true}, {Name: "text", Type: shortcut.FlagString, Desc: "消息内容(支持 Markdown),所有人收到同一条", Required: true}, shortcut.AIMessageTagFlag(), }, Tips: []string{`dws chat +broadcast --to "张三,李四,王五" --text "今晚 8 点上线,请留意"`}, Execute: func(rt *shortcut.RuntimeContext) error { text := rt.Str("text") names := rt.StrSlice("to") if len(names) == 0 { return apperrors.NewValidation("--to 至少要包含一个姓名") } content, _ := json.Marshal(map[string]string{"title": text, "text": text}) var ( sent []string failed []string ) for _, raw := range names { name := strings.TrimSpace(raw) if name == "" { continue } user, err := resolveUser(rt, name) if err != nil { failed = append(failed, fmt.Sprintf("%s(%s)", name, err.Error())) continue } if user.openDingTalkID == "" { failed = append(failed, fmt.Sprintf("%s(通讯录结果缺少 openDingTalkId)", name)) continue } if rt.DryRun() { sent = append(sent, user.name) continue } if _, err := rt.CallMCPWriteData("chat", "send_personal_message", rt.AddAIMessageTag(map[string]any{ "receiverOpenDingTalkId": user.openDingTalkID, "msgType": "markdown", "content": string(content), })); err != nil { failed = append(failed, fmt.Sprintf("%s(发送失败:%s)", name, err.Error())) continue } sent = append(sent, user.name) } if len(sent) == 0 { return apperrors.NewValidation("没有任何人收到消息,请检查姓名是否正确") } result := map[string]any{ "sentCount": len(sent), "failedCount": len(failed), "sent": sent, } if len(failed) > 0 { result["failed"] = failed } return rt.Output(result) }, }
Broadcast: send the SAME single-chat message to several people by NAME.
Steps: split the --to name CSV → resolve each name to a unique user → send an individual single-chat message to each user's openDingTalkId. Names that fail to resolve (unknown / ambiguous) are collected and reported at the end without aborting delivery to the others. Replaces manually running `chat +dm` once per recipient.
dws chat +broadcast --to "张三,李四,王五" --text "今晚 8 点上线,请留意群公告"
var ByMobile = shortcut.Shortcut{ Service: "contact", Command: "+by-mobile", Product: "contact", Description: "按手机号查询某人的完整资料(自动解析 userId 后取详情)", Intent: "当你只知道对方手机号、想一步拿到其完整资料(部门、职位、联系方式、是否管理员等)而不想先按手机号搜出 userId 再单独查详情时使用;" + "内部先用手机号在通讯录里查出对应的 userId,若没有人绑定该手机号会明确报错,再用该 userId 取完整详情。这是纯只读操作,不会修改任何数据。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "mobile", Type: shortcut.FlagString, Desc: "手机号", Required: true}, }, Tips: []string{`dws contact +by-mobile --mobile 13800138000`}, Execute: func(rt *shortcut.RuntimeContext) error { if err := rt.RequireAll("mobile"); err != nil { return err } mobile := rt.Str("mobile") data, err := rt.CallMCPData("contact", "search_user_by_mobile", map[string]any{ "mobile": mobile, }) if err != nil { return err } userID := byMobileExtractUserID(data) if userID == "" { return apperrors.NewValidation( fmt.Sprintf("没找到绑定手机号 %q 的用户;确认号码正确、且该人在当前组织通讯录中。", mobile)) } return rt.CallMCP("get_user_info_by_user_ids", map[string]any{ "user_id_list": []string{userID}, }) }, }
ByMobile: find a person by phone number and return their full profile in one step.
Steps: look the mobile up via search_user_by_mobile → resolve to a userId (error clearly if nobody is bound to that number) → fetch full detail via get_user_info_by_user_ids. Replaces `contact user search-mobile` (copy the userId) → `contact user get --ids <id>`.
dws contact +by-mobile --mobile 13800138000
var CancelEvent = shortcut.Shortcut{ Service: "calendar", Command: "+cancel-event", Product: "calendar", Description: "取消(删除)一个已有日程(删除前先确认它真实存在)", Intent: "当你想取消/删除一个已经存在的日程时使用;" + "内部先用 eventId 拉一次日程详情确认它真实存在并回显标题,再执行删除," + "避免因 eventId 写错而误删别的日程。" + "如果 eventId 查不到会直接报错,不会盲目删除。" + "这是高危写操作,会真实删除该日程,框架会二次确认。", Risk: shortcut.RiskHighWrite, Flags: []shortcut.Flag{ {Name: "event", Type: shortcut.FlagString, Desc: "要取消的日程 eventId(可用 dws calendar event list 查询)", Required: true}, }, Tips: []string{`dws calendar +cancel-event --event EVENT_ID`}, Execute: func(rt *shortcut.RuntimeContext) error { eventID := strings.TrimSpace(rt.Str("event")) if eventID == "" { return apperrors.NewValidation("--event 不能为空") } detail, err := rt.CallMCPData("calendar", "get_calendar_detail", map[string]any{ "eventId": eventID, }) if err != nil { return err } title := cancelEventExtractTitle(detail) if title != "" { _ = rt.Output(map[string]any{ "eventId": eventID, "title": title, "action": "about_to_cancel", }) } return rt.CallMCP("delete_calendar_event", map[string]any{ "eventId": eventID, }) }, }
CancelEvent: cancel (delete) an EXISTING calendar event in one step, with a confirm-before-delete safety net so you never wipe the wrong eventId.
Steps: first pull the event's detail via get_calendar_detail (so a bad or stale eventId fails clearly before any destructive write, and the title is surfaced back to the user). Then delete it via delete_calendar_event. Replaces `calendar event get --id` (verify it's the right one) → `calendar event delete --id` (destroy it).
The eventId param is copied verbatim from the helper's `event get` (get_calendar_detail) and `event delete` (delete_calendar_event) call sites. This is a high-risk write; the framework asks for a second confirmation.
dws calendar +cancel-event --event EVENT_ID
var ChatMessages = shortcut.Shortcut{ Service: "chat", Command: "+chat-messages", Product: "chat", Description: "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", Intent: "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;" + "群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。" + "可选 --time 指定起始时间、--limit 指定每页条数、--direction newer/older 控制时间方向(newer 从给定时间往现在拉,older 往以前拉)。" + "内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。" + "这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群会话 ID(openConversationId),与 --user 互斥"}, {Name: "conversation-id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true}, {Name: "id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true}, {Name: "user", Type: shortcut.FlagString, Desc: "单聊对方的 userId,与 --group 互斥"}, {Name: "time", Type: shortcut.FlagString, Desc: "起始时间,如 \"2025-03-01 00:00:00\"(可选)"}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页拉取的消息条数(可选)"}, {Name: "size", Type: shortcut.FlagInt, Desc: "--limit 的旧版别名", Hidden: true}, {Name: "direction", Type: shortcut.FlagString, Desc: "时间方向 newer/older(可选,newer 从给定时间往现在拉,older 往以前拉)"}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id", "id", "user"}}, }, Tips: []string{ `dws chat +chat-messages --group <openconversation_id> --time "2025-03-01 00:00:00"`, `dws chat +chat-messages --user <userId> --time "2025-03-01 00:00:00" --limit 50`, `dws chat +chat-messages --group <openconversation_id> --direction older`, }, Execute: func(rt *shortcut.RuntimeContext) error { // Step 1 — build params and pick the right tool. Param keys // (openconversation_id / userId / time / forward / limit) match the MCP server // schema for group and direct message listing. var tool string params := map[string]any{} if rt.Changed("time") && rt.Str("time") != "" { params["time"] = rt.Str("time") } if limit := rt.IntFirst("limit", "size"); limit > 0 { params["limit"] = limit } if rt.Changed("direction") { switch strings.TrimSpace(strings.ToLower(rt.Str("direction"))) { case "newer": params["forward"] = true case "older": params["forward"] = false } } if group := rt.StrFirst("group", "conversation-id", "id"); group != "" { tool = "list_conversation_message_v2" params["openconversation_id"] = group } else { tool = "list_individual_chat_message" params["userId"] = rt.Str("user") } data, err := rt.CallMCPData("chat", tool, params) if err != nil { return err } items := chatMessageItems(data) results := make([]map[string]any, 0, len(items)) for _, m := range items { results = append(results, map[string]any{ "sender": chatMessageSender(m), "text": chatMessageText(m), "createTime": chatMessageCreateTime(m), }) } return rt.Output(map[string]any{ "messages": results, "count": len(results), }) }, }
ChatMessages: fetch the message list of one conversation (group OR single chat) and print a clean projected list (speaker / text / time) instead of a raw dump.
Steps:
- depending on whether --group or --user is given, call either list_conversation_message_v2 (group; param openconversation_id) or list_individual_chat_message (single chat; param userId) on the chat server — tool names and param keys copied verbatim from chat.go's `dws chat message list` call sites;
- defensively unwrap the message list (multiple candidate container keys) and project each message to {sender, text, createTime} tolerating field aliases and one level of nesting;
- print via rt.Output as {messages, count} so it honours --format/--jq/--fields.
Read-only: it only reads a conversation's messages and reshapes them locally, never posts or mutates anything.
dws chat +chat-messages --group <openconversation_id> --time "2025-03-01 00:00:00" dws chat +chat-messages --user <userId> --time "2025-03-01 00:00:00" --limit 50
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 { 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) }) 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, }) }, }
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 CreatedTodos = shortcut.Shortcut{ Service: "todo", Command: "+created-todos", Product: "todo", Description: "列出我创建的待办(我作为创建人 creator 发起的待办,而非分配给我执行的)", Intent: "当你想快速看清『哪些待办是我自己创建/发起的』,而不是别人指派给我执行的待办时使用;" + "内部拉取你当前组织下角色为创建人(creator)的待办列表(roleTypes=[\"creator\"]," + "creator 是待办列表支持的角色枚举之一),再在本地把每条待办投影成标题(subject)、" + "任务 ID(taskId) 和截止时间(dueTime) 打印出来。这是纯只读操作,只做列表与投影," + "不会创建或修改任何待办;若没有你创建的待办则返回空列表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{}, Tips: []string{ `dws todo +created-todos`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("todo", "get_user_todos_in_current_org", map[string]any{ "pageNum": "1", "pageSize": "50", "roleTypes": []string{"creator"}, }) if err != nil { return err } cards := shortcutTodoCards(data) created := make([]map[string]any, 0, len(cards)) for _, m := range cards { subject, _ := m["subject"].(string) item := map[string]any{ "subject": subject, "taskId": shortcutTodoTaskID(m), } if due, ok := shortcutOverdueDueTime(m); ok { item["dueTime"] = due } created = append(created, item) } return rt.Output(map[string]any{"created": created}) }, }
CreatedTodos: list the todos I created (rather than the ones assigned to me to execute) in one step.
Steps:
list my todos via get_user_todos_in_current_org (pageNum / pageSize as strings, mirroring helpers.todo list) but with roleTypes=["creator"] so the server returns todos where I am the creator. "creator" is one of the three enum values accepted by helpers.parseRoleTypes (creator / executor / participant), so this is a supported role value.
project each todoCards[] entry to {subject, taskId, dueTime} with defensive field parsing (reusing shortcutTodoCards / shortcutTodoTaskID / shortcutOverdueDueTime), and print via rt.Output so it honours --format/--jq/--fields.
Read-only: it only lists and projects, it never creates or mutates any todo.
dws todo +created-todos
var DM = shortcut.Shortcut{ Service: "chat", Command: "+dm", Product: "chat", Description: "按姓名直接给某人发单聊消息(自动解析 userId)", Intent: "当你只知道对方姓名、想直接发一条单聊消息而不想先查 userId 时使用;" + "内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "to", Type: shortcut.FlagString, Desc: "收件人姓名/花名", Required: true}, {Name: "text", Type: shortcut.FlagString, Desc: "消息内容(支持 Markdown)", Required: true}, shortcut.AIMessageTagFlag(), }, Tips: []string{`dws chat +dm --to 张三 --text "周报发我一下"`}, Execute: func(rt *shortcut.RuntimeContext) error { text := rt.Str("text") user, err := resolveUser(rt, rt.Str("to")) if err != nil { return err } if user.openDingTalkID == "" { return apperrors.NewValidation("通讯录结果缺少 openDingTalkId,无法发送单聊消息;请改用 chat +messages-send --open-dingtalk-id") } content, _ := json.Marshal(map[string]string{"title": text, "text": text}) return rt.CallMCP("send_personal_message", rt.AddAIMessageTag(map[string]any{ "receiverOpenDingTalkId": user.openDingTalkID, "msgType": "markdown", "content": string(content), })) }, }
DM: message a person by NAME, no ID juggling.
Steps: resolve name → single user (disambiguate on multiple matches) → send a single-chat message via openDingTalkId. Replaces `contact +search-user` (copy openDingTalkId) → `chat +messages-send --open-dingtalk-id <id>`.
dws chat +dm --to 张三 --text "周报发我一下"
var DeptMembers = shortcut.Shortcut{ Service: "contact", Command: "+dept-members", Product: "contact", Description: "按部门名列出部门成员(自动解析 deptId)", Intent: "当你只知道某个部门的名称、想知道该部门里都有哪些成员时使用;" + "内部先按部门名搜索出唯一的 deptId(匹配到 0 个或多个时不猜测," + "而是列出候选部门名与 deptId 让你选),再打印该部门的直接成员列表" + "(仅本部门,不递归下级部门)。只读,不做任何修改。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "dept", Type: shortcut.FlagString, Desc: "部门名/关键词", Required: true}, }, Tips: []string{`dws contact +dept-members --dept 技术部`}, Execute: func(rt *shortcut.RuntimeContext) error { if err := rt.RequireAll("dept"); err != nil { return err } keyword := strings.TrimSpace(rt.Str("dept")) data, err := rt.CallMCPData("contact", "search_dept_by_keyword", map[string]any{ "query": keyword, }) if err != nil { return err } depts := deptMembersExtractDepts(data) switch { case len(depts) == 0: return apperrors.NewValidation(fmt.Sprintf( "没找到名字里包含 %q 的部门;换个更准确的部门名再试。", keyword)) case len(depts) > 1: return apperrors.NewValidation(fmt.Sprintf( "%q 匹配到 %d 个部门:%s。请用更精确的部门名,"+ "或直接用 dws contact dept list-members --depts <deptId>。", keyword, len(depts), strings.Join(deptMembersLabels(depts), "、"))) } return rt.CallMCP("get_dept_members_by_deptId", map[string]any{ "deptIds": []string{strconv.FormatInt(depts[0].id, 10)}, }) }, }
DeptMembers: list the members of a department, by NAME, in one command.
Steps: search the directory for the department by keyword (search_dept_by_keyword) → defensively parse the unique deptId out of the response → print that department's direct members (get_dept_members_by_deptId). Replaces the manual dance of `contact dept search --query <名称>` → copy deptId → `contact dept list-members --depts <deptId>`.
Disambiguation: if the name matches zero or several departments, it does NOT guess — it errors and lists the candidates (name + deptId) so the caller can re-run against a specific department.
Scope note: get_dept_members_by_deptId returns only the direct members of the resolved department, it does NOT recurse into sub-departments.
dws contact +dept-members --dept 技术部
var DocAppend = shortcut.Shortcut{ Service: "doc", Command: "+doc-append", Product: "doc", Description: "在文档末尾追加一段文本(安全追加,不改动原有内容)", Intent: "当你只想往一篇钉钉文档的最后面补一段文字、又不想动原有内容时使用;" + "内部用文档更新的“追加(append)”模式,把你给的文本安全地拼到文档末尾," + "不需要你先去查文档块列表、算末尾位置或手工拼块结构。" + "会真实写入文档内容。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "doc", Type: shortcut.FlagString, Desc: "文档 documentId / nodeId(或文档 URL/token)", Required: true}, {Name: "text", Type: shortcut.FlagString, Desc: "要追加到文档末尾的文本", Required: true}, }, Tips: []string{ `dws doc +doc-append --doc DOC_ID --text "补充说明:本方案已评审通过。"`, `dws doc +doc-append --doc "https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>" --text "追加一行备注"`, }, Execute: func(rt *shortcut.RuntimeContext) error { nodeID := strings.TrimSpace(rt.Str("doc")) if nodeID == "" { return apperrors.NewValidation("--doc 不能为空,请提供文档 documentId/nodeId 或文档 URL") } text := rt.Str("text") if strings.TrimSpace(text) == "" { return apperrors.NewValidation("--text 不能为空,请提供要追加的文本") } return rt.CallMCP("update_document", map[string]any{ "nodeId": nodeID, "markdown": text, "mode": "append", }) }, }
DocAppend: append a chunk of text to the END of a document in one command.
Instead of asking you to first list the document blocks, figure out the child count / last-block index, then hand-build an insert_document_block element payload, this shortcut leans on update_document's built-in "append" mode — the helper documents mode=append as "追加 (在末尾追加,最安全)", i.e. it appends the given markdown to the document tail safely without touching existing content. Params (nodeId/markdown/mode) are copied verbatim from the update_document append call site in internal/helpers/doc.go.
dws doc +doc-append --doc DOC_ID --text "今天的会议结论:下周一上线。"
var DoneApprovals = shortcut.Shortcut{ Service: "oa", Command: "+done-approvals", Product: "oa", Description: "只读列出我已处理过的审批任务(审批历史)并投影为可读列表", Intent: "当你只想快速回看「我已经处理过(同意/拒绝)」的审批任务历史——每条的标题、发起人、审批实例 ID 和创建时间——" + "而不想拿到一大坨原始字段时使用;内部拉取你的已办审批单,再在本地投影出可读字段。" + "这是纯只读操作,只做列出与本地投影,绝不会同意、拒绝或以任何方式提交/修改任何审批;" + "若没有任何已处理记录则提示「没有已处理的审批记录」。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "limit", Type: shortcut.FlagInt, Desc: "最多列出多少条(可选)", Required: false}, }, Tips: []string{ `dws oa +done-approvals`, `dws oa +done-approvals --limit 10`, }, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{ "pageNumber": float64(1), "pageSize": float64(20), } if rt.Changed("limit") { if n := rt.Int("limit"); n > 0 { params["pageSize"] = float64(n) } } data, err := rt.CallMCPData("oa", "get_done_tasks", params) if err != nil { return err } items := shortcutApproveItems(data) results := make([]map[string]any, 0, len(items)) for _, m := range items { results = append(results, map[string]any{ "title": shortcutApproveTitle(m), "processInstanceId": shortcutApproveInstanceID(m), "originatorName": pendingApprovalsOriginator(m), "createTime": pendingApprovalsCreateTime(m), }) } if len(results) == 0 { return apperrors.NewValidation("没有已处理的审批记录") } return rt.Output(map[string]any{"count": len(results), "done": results}) }, }
DoneApprovals: READ-ONLY list of the approval tasks I have already processed (my approval history), projected to clean fields. Unlike `oa +approve-by` (which agrees to an approval) or `oa +pending` (which lists what still waits for me), this shortcut only looks back at what I've already handled — it never approves / rejects / mutates anything.
Steps (tool name + param keys copied verbatim from helpers/oa.go):
list my done approvals via get_done_tasks with pageNumber / pageSize as float64, mirroring `dws oa approval list-executed`; optional --limit maps to pageSize (float64). This tool takes NO time window (unlike list_pending_approvals) — only pageNumber/pageSize/query.
defensively unwrap the returned instance list (reusing shortcutApproveItems from approve.go, multiple candidate container keys + one nested level) and project each entry to a readable shape {title, originatorName, processInstanceId, createTime}, every field probed across candidate keys.
if nothing has been processed, report "没有已处理的审批记录" instead of an empty raw dump.
dws oa +done-approvals dws oa +done-approvals --limit 10
var DueToday = shortcut.Shortcut{ Service: "todo", Command: "+due-today", Product: "todo", Description: "列出我今天到期的待办", Intent: "当你想快速看清自己今天(planFinishDate 落在今天 00:00 到次日 00:00 之间)到期的待办、方便安排一天的工作时使用;" + "内部按今天的本地时间窗,把 planFinishDateStart=今天0点、planFinishDateEnd=次日0点(毫秒时间戳)传给 get_user_todos_in_current_org 做服务端过滤," + "默认拉取你作为执行人(executor)的待办,可用 --role-types 覆盖角色范围,最后只打印这些今天到期待办的标题、状态、优先级、创建人、到期时间和任务 ID。" + "这与 +overdue(已过期)不同:+overdue 看的是已经过了截止时间的待办,本命令看的是今天当天到期的待办。" + "这是纯只读操作,只做列表与投影,不会修改或完成任何待办;若今天没有到期的待办则返回错误提示。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ { Name: "role-types", Type: shortcut.FlagString, Desc: "覆盖默认角色范围,逗号分隔,取值 creator/executor/participant;不传则默认 executor", }, }, Tips: []string{ `dws todo +due-today`, `dws todo +due-today --role-types creator,executor`, }, Execute: func(rt *shortcut.RuntimeContext) error { now := time.Now() loc := now.Location() startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) startOfNextDay := startOfDay.AddDate(0, 0, 1) startMs := startOfDay.UnixMilli() endMs := startOfNextDay.UnixMilli() roleTypes := []string{"executor"} if rt.Changed("role-types") { parsed, err := parseRelatedRoleTypes(rt.Str("role-types")) if err != nil { return err } if len(parsed) > 0 { roleTypes = parsed } } params := map[string]any{ "roleTypes": roleTypes, "planFinishDateStart": startMs, "planFinishDateEnd": endMs, } cards, err := shortcutListAllTodoCards(rt, params) if err != nil { return err } results := make([]map[string]any, 0, len(cards)) for _, m := range cards { if due, ok := shortcutOverdueDueTime(m); ok { if due < startMs || due >= endMs { continue } } taskID := shortcutRelatedTaskID(m) results = append(results, shortcutRelatedProject(m, taskID)) } if len(results) == 0 { return apperrors.NewValidation("今天没有到期的待办") } return rt.Output(map[string]any{"count": len(results), "tasks": results}) }, }
DueToday: list MY todos whose planFinishDate falls within today.
Unlike +overdue (which locally keeps cards whose dueTime is already in the past), this shortcut filters SERVER-SIDE: it passes the today window as planFinishDateStart / planFinishDateEnd (epoch millis) to get_user_todos_in_current_org, so the backend returns only todos due today.
Steps:
compute the [today 00:00, tomorrow 00:00) window in the local timezone, as epoch millis;
list my todos via get_user_todos_in_current_org with planFinishDateStart = today 00:00 ms, planFinishDateEnd = tomorrow 00:00 ms, roleTypes=["executor"] (mirroring +overdue), pageNum/pageSize as strings (mirroring helpers.todo list);
project each card to a clean {title, status, priority, creator, planFinishDate, taskId} shape and print via rt.Output so it honours --format/--jq/--fields.
Read-only: it never mutates any todo, it only lists and projects.
dws todo +due-today
var FindDoc = shortcut.Shortcut{ Service: "doc", Command: "+find-doc", Product: "doc", Description: "按关键词搜索云文档并投影关键字段(只读)", Intent: "当你只记得云文档标题或内容里的某个关键词,想快速按关键词找到匹配的文档、拿到它的标题、URL、类型和 token 以便后续查看或编辑," + "却不想拿到一大坨原始字段时使用;内部调用云文档的 search_documents 工具,把 --query 作为搜索关键词(keyword)," + "可选地用 --limit 限制返回条数(pageSize),再在本地把每条命中结果精简为「标题、URL、类型、token」四个字段后打印。" + "这是纯只读操作,只做搜索与本地投影,不会创建、修改或删除任何文档;未命中时提示「没搜到文档」。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "按关键词搜索云文档(必填)", Required: true}, {Name: "limit", Type: shortcut.FlagInt, Desc: "限制返回的文档条数(可选)"}, }, Tips: []string{ `dws doc +find-doc --query 季度汇报`, `dws doc +find-doc --query 合同 --limit 10`, }, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{ "keyword": rt.Str("query"), } if rt.Changed("limit") { if n := rt.Int("limit"); n > 0 { params["pageSize"] = n } } data, err := rt.CallMCPData("doc", "search_documents", params) if err != nil { return err } items := shortcutFindDocItems(data) results := make([]map[string]any, 0, len(items)) for _, m := range items { results = append(results, map[string]any{ "title": shortcutFindDocStr(m, "title", "name", "docName", "subject"), "url": shortcutFindDocStr(m, "url", "docUrl", "link", "webUrl"), "type": shortcutFindDocStr(m, "docType", "type", "dentryType", "fileType"), "token": shortcutFindDocStr(m, "docId", "token", "nodeId", "dentryId", "id"), }) } if len(results) == 0 { return apperrors.NewValidation("没搜到文档") } return rt.Output(map[string]any{"documents": results, "count": len(results)}) }, }
FindDoc: search cloud documents by keyword and project the essentials.
This is a one-step convenience wrapper over the doc MCP tool search_documents. It mirrors the helpers doc search path exactly:
- keyword ← --query (the free-text search term; MCP arg "keyword", verbatim)
- pageSize ← --limit (optional; MCP arg "pageSize", verbatim)
The raw search response is then reduced locally to a compact list of {title, url, type, token} per hit and emitted via rt.Output, so it honours the root --format/--jq/--fields projection flags. Field parsing is defensive: both the container (result/data/list/items/documents/docs) and each item's fields (multiple candidate keys) are probed leniently.
Read-only: it never mutates anything, it only searches and projects locally.
dws doc +find-doc --query 季度汇报
var FindFile = shortcut.Shortcut{ Service: "drive", Command: "+find-file", Product: "drive", Description: "按名称关键词搜索钉盘文件并投影关键字段(只读)", Intent: "当你只记得钉盘文件的名字(或其中一部分),想快速按文件名关键词找到它、拿到它的 dentryId 以便后续下载/查看," + "却不想手动翻目录或写复杂过滤条件时使用;内部调用钉盘的 search_files 工具,把 --query 作为文件名关键词(keyword) " + "并限定搜索范围为钉盘文件(searchTarget=file),再在本地把每条命中结果精简为「文件名、类型、dentryId、大小」四个字段后打印。" + "这是纯只读操作,只做搜索与本地投影,不会创建、移动或删除任何文件;未命中时返回空列表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "文件名关键词(必填)", Required: true}, }, Tips: []string{ `dws drive +find-file --query 季度汇报`, `dws drive +find-file --query 合同`, }, Execute: func(rt *shortcut.RuntimeContext) error { keyword := strings.TrimSpace(rt.Str("query")) data, err := rt.CallMCPData("drive", "search_files", map[string]any{ "keyword": keyword, "searchTarget": "file", }) if err != nil { return err } items := shortcutFindFileItems(data) files := make([]map[string]any, 0, len(items)) for _, m := range items { files = append(files, map[string]any{ "name": shortcutFindFileStr(m, "name", "fileName", "title", "dentryName"), "type": shortcutFindFileStr(m, "type", "dentryType", "extension", "fileType"), "dentryId": shortcutFindFileStr(m, "dentryId", "dentryUuid", "fileId", "nodeId", "id"), "fileSize": shortcutFindFileSize(m), }) } return rt.Output(map[string]any{"files": files}) }, }
FindFile: search 钉盘 files by name keyword and project the essentials.
This is a one-step convenience wrapper over the drive MCP tool search_files. It mirrors helpers driveSearchCmd exactly for the "仅搜钉盘文件" path:
- keyword ← --query (the free-text file-name search term; MCP arg "keyword")
- searchTarget ← "file" (restrict to 钉盘 files/folders, no doc-space aggregation)
The raw search response is then reduced locally to a compact list of {name, type, dentryId, fileSize} per hit and emitted via rt.Output, so it honours the root --format/--jq/--fields projection flags. Field parsing is defensive: both the container (result/data/items/files/nodes/list) and each item's fields (multiple candidate keys) are probed leniently.
Read-only: it never mutates anything, it only searches and projects locally.
dws drive +find-file --query 季度汇报
var FindMailUser = shortcut.Shortcut{ Service: "mail", Command: "+find-mail-user", Product: "mail", Description: "按关键词搜索邮箱联系人并投影列表(姓名/昵称/邮箱/工号等)", Intent: "当你只知道某人的姓名、花名或邮箱片段,想在企业邮箱通讯录里按关键词把匹配的邮箱用户找出来、" + "并只看一份精简清单(姓名、昵称、邮箱地址、工号、职位、工作地)而不想拿到一大坨原始字段时使用;" + "内部按 --query 关键词调用邮箱用户搜索,再在本地把每个匹配用户投影成整洁记录打印出来,可配合 --format/--jq/--fields。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何数据;" + "注意仅企业邮箱可用(个人邮箱如 xxx@dingtalk.com 会因无权限报错);若没有命中则提示「没搜到邮箱联系人」。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词(姓名/花名/邮箱片段,必填)", Required: true}, {Name: "limit", Type: shortcut.FlagInt, Desc: "返回条数上限(可选)", Required: false}, }, Tips: []string{ `dws mail +find-mail-user --query "张三"`, `dws mail +find-mail-user --query alice --limit 10`, }, Execute: func(rt *shortcut.RuntimeContext) error { args := map[string]any{"keyword": rt.Str("query")} if rt.Changed("limit") { args["size"] = strconv.Itoa(rt.Int("limit")) } data, err := rt.CallMCPData("mail", "search_mail_users", args) if err != nil { return err } users := findMailUserUnwrap(data) results := make([]map[string]any, 0, len(users)) for _, u := range users { results = append(results, map[string]any{ "name": findMailUserString(u, "name", "displayName", "userName", "nick"), "nickname": findMailUserString(u, "nickname", "nick", "displayName"), "email": findMailUserString(u, "email", "mail", "emailAddress", "address", "account"), "employeeNo": findMailUserAny(u, "employeeNo", "employeeNumber", "jobNumber"), "jobTitle": findMailUserString(u, "jobTitle", "title", "position"), "workLocation": findMailUserString(u, "workLocation", "location", "workPlace"), "id": findMailUserAny(u, "id", "userId", "userid"), }) } if len(results) == 0 { return apperrors.NewValidation("没搜到邮箱联系人") } return rt.Output(map[string]any{"users": results, "count": len(results)}) }, }
FindMailUser: search mailbox users by keyword (name / nickname / email fragment) and project a compact contact list in one step.
Steps:
call search_mail_users with keyword=--query (and size=--limit when given). The tool name, server ("mail") and the "keyword"/"size" argument keys are taken verbatim from helpers.mail.go's `mail user search` command (callMCPTool("search_mail_users", …) with toolArgs["keyword"]/["size"]);
in Go, defensively unwrap the user list and project each entry to {name, nickname, email, employeeNo, jobTitle, workLocation, id} — field parsing probes several candidate keys — and print via rt.Output so it honours --format/--jq/--fields;
if nothing matched, report "没搜到邮箱联系人" instead of an empty raw dump.
Read-only: it only searches and reshapes, never mutating anything.
Note: per the helper docs this only works for enterprise mailboxes (not @dingtalk.com personal mailboxes).
dws mail +find-mail-user --query "张三" dws mail +find-mail-user --query alice --limit 10
var FindRecord = shortcut.Shortcut{ Service: "aitable", Command: "+find-record", Product: "aitable", Description: "在指定多维表里按关键词查记录(只读)", Intent: "当你已经知道某个多维表的 baseId 和 tableId、想按一个关键词快速找出匹配的行记录," + "却不想手写结构化过滤条件时使用;内部直接调用 query_records,把 --query 作为全文关键词(keyword)在该表里检索并打印匹配记录。" + "不传 --query 时则返回该表的前若干条记录。这是只读操作,不会修改任何数据。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "base", Type: shortcut.FlagString, Desc: "Base ID(多维表所属 base)", Required: true}, {Name: "table", Type: shortcut.FlagString, Desc: "Table ID(要检索的数据表)", Required: true}, {Name: "query", Type: shortcut.FlagString, Desc: "全文关键词(可选,不填则取前若干条)", Required: false}, }, Tips: []string{ `dws aitable +find-record --base B --table T`, `dws aitable +find-record --base B --table T --query 张三`, }, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{ "baseId": rt.Str("base"), "tableId": rt.Str("table"), } if kw := strings.TrimSpace(rt.Str("query")); kw != "" { params["keyword"] = kw } return rt.CallMCP("query_records", params) }, }
FindRecord: search records in a given aitable (多维表) by a full-text keyword.
This is a one-step convenience wrapper over query_records. It mirrors helpers RecordQuery exactly:
- baseId ← --base
- tableId ← --table
- keyword ← --query (the free-text search term; MCP arg is "keyword")
When --query is omitted it simply returns the first page of records for the table (no filter). It is a read-only operation and never mutates data.
dws aitable +find-record --base B --table T dws aitable +find-record --base B --table T --query 张三
var FindRoom = shortcut.Shortcut{ Service: "calendar", Command: "+find-room", Product: "calendar", Description: "查询指定时间段内所有可用的会议室", Intent: "当你想在某个明确的时间段内找出所有当前可预定的空闲会议室(比如临时要约线下会、先看看哪些会议室有空)时使用;" + "内部把你给的 ISO8601 起止时间解析成毫秒时间戳,调用会议室可用性查询,只返回该时间范围内可预定的会议室," + "并投影出每个会议室的 roomId、名称与容量,方便你随后用来预订。" + "这是纯只读操作,只做可用性查询,不会预订或改动任何会议室或日程;" + "注意大部分会议室仅在工作时间可用,非工作时间可能查不到结果,且 start 需为未来时间。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {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}, }, Tips: []string{ `dws calendar +find-room --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00"`, }, Execute: func(rt *shortcut.RuntimeContext) error { startMillis, err := findRoomParseMillis("start", rt.Str("start")) if err != nil { return err } endMillis, err := findRoomParseMillis("end", rt.Str("end")) if err != nil { return err } if endMillis <= startMillis { return apperrors.NewValidation("--end 必须晚于 --start") } data, err := rt.CallMCPData("calendar", "query_available_meeting_room", map[string]any{ "startTime": startMillis, "endTime": endMillis, }) if err != nil { return err } rooms := make([]map[string]any, 0) for _, m := range findRoomExtractRooms(data) { rooms = append(rooms, map[string]any{ "roomId": findRoomFirstString(m, "roomId", "roomID", "id", "room_id"), "name": findRoomFirstString(m, "roomName", "name", "title", "displayName"), "capacity": findRoomCapacity(m), }) } return rt.Output(map[string]any{"rooms": rooms}) }, }
FindRoom: list meeting rooms that are AVAILABLE within a given time window.
Steps:
parse the ISO8601 --start/--end into epoch millis, exactly as the calendar room-search tool expects (startTime/endTime are int64 millis, mirroring helpers.roomSearch Mode 2);
call query_available_meeting_room with {startTime, endTime} — the same MCP tool + parameter names used by `calendar room search` availability mode (see helpers/calendar.go callMeetingRoomSearchResult);
defensively project each returned room to {roomId, name, capacity} and print the list via rt.Output so it honours --format/--jq/--fields.
Read-only: it only queries availability, it never books or mutates anything.
dws calendar +find-room --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00"
var FreeBusy = shortcut.Shortcut{ Service: "calendar", Command: "+free", Product: "calendar", Description: "按姓名查询某人在指定时间段内的忙闲状态(自动解析 userId)", Intent: "当你只知道对方姓名、想知道 TA 在某段时间内是空闲还是被日程占用(比如约会前先看看有没有空)而不想先手动查 userId 时使用;" + "内部先按姓名搜通讯录解析出唯一 userId,姓名匹配到多人时会列出候选让你区分,再按时间范围查询忙闲。只读,不产生任何日程变更。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "who", 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-10T18:00:00+08:00)", Required: true}, }, Tips: []string{`dws calendar +free --who 张三 --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T18:00:00+08:00"`}, Execute: func(rt *shortcut.RuntimeContext) error { user, err := resolveUser(rt, rt.Str("who")) if err != nil { return err } startMillis, err := freebusyParseMillis("start", rt.Str("start")) if err != nil { return err } endMillis, err := freebusyParseMillis("end", rt.Str("end")) if err != nil { return err } if endMillis <= startMillis { return apperrors.NewValidation("--end 必须晚于 --start") } data, err := rt.CallMCPData("calendar", "query_busy_status", map[string]any{ "startTime": startMillis, "endTime": endMillis, "userIds": []string{user.userID}, }) if err != nil { return err } busy := freebusySlots(data) return rt.Output(map[string]any{ "who": user.name, "userId": user.userID, "busy": busy, "free": len(busy) == 0, }) }, }
FreeBusy: check whether a person is busy or free in a time window, by NAME.
Steps: resolve the person's name → unique userId → query their busy/free status over the given range. Replaces `contact +search-user` (copy userId) → `calendar busy search --users <id> --start ... --end ...`.
dws calendar +free --who 张三 --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T18:00:00+08:00"
var FreeSlots = shortcut.Shortcut{ Service: "calendar", Command: "+free-slots", Product: "calendar", Description: "找我某天工作时段内的空闲时间段(默认今天 09:00-18:00)", Intent: "当你想知道『我今天(或某天)还有哪些时间是空的、可以安排会议/事情』时使用;" + "内部列出目标日期(默认今天,--in-days 指定几天后)的全部日程,合并忙碌时段," + "再在工作时间范围内(默认 09:00-18:00,可用 --from/--to 指定起止小时)算出所有空闲窗口并给出每段的起止与时长。" + "只读操作,不修改任何日程;用于快速定位可预约的空档。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "in-days", Type: shortcut.FlagInt, Desc: "几天后(可选,0=今天默认)", Required: false}, {Name: "from", Type: shortcut.FlagInt, Desc: "工作时段起始小时(可选,默认 9)", Required: false}, {Name: "to", Type: shortcut.FlagInt, Desc: "工作时段结束小时(可选,默认 18)", Required: false}, }, Tips: []string{ `dws calendar +free-slots`, `dws calendar +free-slots --in-days 1 --from 9 --to 20`, }, Execute: func(rt *shortcut.RuntimeContext) error { now := time.Now() offset := rt.Int("in-days") day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, offset) fromHour, toHour := 9, 18 if rt.Changed("from") { fromHour = rt.Int("from") } if rt.Changed("to") { toHour = rt.Int("to") } if fromHour < 0 || fromHour > 23 || toHour < 1 || toHour > 24 || toHour <= fromHour { return apperrors.NewValidation("--from/--to 需满足 0<=from<to<=24") } workStart := day.Add(time.Duration(fromHour) * time.Hour) workEnd := day.Add(time.Duration(toHour) * time.Hour) data, err := rt.CallMCPData("calendar", "list_calendar_events", map[string]any{ "startTime": day.UnixMilli(), "endTime": day.AddDate(0, 0, 1).UnixMilli(), "calendarId": "primary", }) if err != nil { return err } // Collect and clip busy intervals to the work window. type interval struct{ start, end time.Time } var busy []interval for _, e := range shortcutNextEventList(data) { s, ok := shortcutNextEventStart(e) if !ok { continue } en, ok := conflictsEndTime(e) if !ok { continue } if en.After(workStart) && s.Before(workEnd) { if s.Before(workStart) { s = workStart } if en.After(workEnd) { en = workEnd } busy = append(busy, interval{s, en}) } } sort.Slice(busy, func(i, j int) bool { return busy[i].start.Before(busy[j].start) }) free := make([]map[string]any, 0) cursor := workStart for _, b := range busy { if b.start.After(cursor) { free = append(free, freeSlotEntry(cursor, b.start)) } if b.end.After(cursor) { cursor = b.end } } if cursor.Before(workEnd) { free = append(free, freeSlotEntry(cursor, workEnd)) } return rt.Output(map[string]any{ "date": day.Format("2006-01-02"), "window": fmt.Sprintf("%02d:00-%02d:00", fromHour, toHour), "slotCount": len(free), "freeSlots": free, "allBusy": len(free) == 0, }) }, }
FreeSlots: find the OPEN gaps in my day — "when can I fit a meeting?" — the complement of +conflicts. Not offered by the raw 1:1 layer.
It lists list_calendar_events for the target day (default today, --in-days N ahead), merges the busy intervals, and reports the free windows inside a working-hours range (default 09:00–18:00, override with --from/--to hour). Read-only.
dws calendar +free-slots dws calendar +free-slots --in-days 1 --from 9 --to 20
var GroupMembers = shortcut.Shortcut{ Service: "chat", Command: "+group-members", Product: "chat", Description: "按群名列出群成员(自动搜群解析 openConversationId)", Intent: "当你只知道群的名字、想看看这个群里有哪些成员,而不想先手动查群 ID 时使用;" + "内部先按群名搜索群聊解析出唯一 openConversationId,再拉取该群的成员列表。" + "群名匹配到多个群时会列出候选让你区分、绝不自行假定。只读,不改动任何数据。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群名称(搜群关键词,用群名里连续的核心词)", Required: true}, }, Tips: []string{`dws chat +group-members --group 项目冲刺`}, Execute: func(rt *shortcut.RuntimeContext) error { groupName := rt.Str("group") data, err := rt.CallMCPData("im", "search_groups", map[string]any{ "keyword": groupName, "limit": 10, "cursor": "0", }) if err != nil { return err } groups := extractGroupsForSend(data) switch { case len(groups) == 0: return apperrors.NewValidation(fmt.Sprintf( "没找到名字匹配 %q 的群;换用群名里连续的核心词再试。", groupName)) case len(groups) > 1: return apperrors.NewValidation(fmt.Sprintf( "%q 匹配到 %d 个群:%s。请用更精确的群名,或直接用 dws chat group members --id <openConversationId> 指定群。", groupName, len(groups), strings.Join(sendGroupLabels(groups), "、"))) } mdata, err := rt.CallMCPData("chat", "get_group_members", map[string]any{ "openconversation_id": groups[0].id, }) if err != nil { return err } members := groupMemberProject(mdata) if len(members) == 0 { return rt.Output(mdata) } return rt.Output(map[string]any{"count": len(members), "members": members}) }, }
GroupMembers: list a group's members by its NAME, no openConversationId juggling.
Steps: search groups by name → resolve to a single openConversationId (disambiguate on multiple matches, never guess) → list that group's members. Replaces `chat search --query <群名>` (copy openConversationId) → `chat group members --id <openConversationId>`.
Note: the group lookup uses `search_groups` (im server, keyword search over group NAMES) — NOT `search_common_groups`, which searches by member nicknames and cannot locate a group by its title.
dws chat +group-members --group 项目冲刺
var Invite = shortcut.Shortcut{ Service: "calendar", Command: "+invite", Product: "calendar", Description: "按姓名把参会人加入已有日程(自动解析 userId 后批量添加)", Intent: "当你已经有一个日程(知道 eventId),想按姓名把几位同事拉进来当参会人时使用;" + "内部先把 --with 里每个姓名解析成唯一 userId,再一次性把他们全部加到 --event 指定的日程里。" + "会真实修改日程并发出参会邀请。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "event", Type: shortcut.FlagString, Desc: "已有日程的 eventId", Required: true}, {Name: "with", Type: shortcut.FlagString, Desc: "参会人姓名,逗号分隔", Required: true}, }, Tips: []string{ `dws calendar +invite --event EVENT_ID --with 张三`, `dws calendar +invite --event EVENT_ID --with 张三,李四`, }, Execute: func(rt *shortcut.RuntimeContext) error { eventID := strings.TrimSpace(rt.Str("event")) if eventID == "" { return apperrors.NewValidation("--event 需要一个有效的日程 eventId") } // Resolve every participant name to a unique userId first, so an // unknown/ambiguous name fails before we touch the event. 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 需要至少一个有效的参会人姓名") } return rt.CallMCP("add_calendar_participant", map[string]any{ "eventId": eventID, "attendeesToAdd": userIDs, }) }, }
Invite: add people BY NAME as participants to an EXISTING calendar event.
Steps: resolve every name in --with to a unique userId, then batch-add them all to the --event day's event as attendees. Replaces the manual flow of `contact +search-user` (copy each userId) → `calendar attendee add`.
dws calendar +invite --event EVENT_ID --with 张三,李四
var LatestMinutes = shortcut.Shortcut{ Service: "minutes", Command: "+latest-minutes", Product: "minutes", Description: "取我最新的一条妙记(听记)详情", Intent: "当你只想快速看回自己最近的一条会议听记,却不想先翻列表、复制 taskUuid 再查详情时使用;" + "内部先列出你创建的听记(可用 --keyword 缩小范围),自动挑出最新的一条,再拉取它的基础信息(标题、创建人、时间、访问链接等)。" + "这是只读操作,不会修改任何听记;若你名下没有任何听记则提示「暂无妙记」。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "keyword", Type: shortcut.FlagString, Desc: "按关键字过滤听记(可选)", Required: false}, }, Tips: []string{ `dws minutes +latest-minutes`, `dws minutes +latest-minutes --keyword 周会`, }, Execute: func(rt *shortcut.RuntimeContext) error { listArgs := map[string]any{ "belongingConditionId": "created", "maxResults": float64(20), } if kw := rt.Str("keyword"); kw != "" { listArgs["keyword"] = kw } data, err := rt.CallMCPData("minutes", "list_by_keyword_and_time_range", listArgs) if err != nil { return err } taskUUID := latestMinutesTaskUUID(data) if taskUUID == "" { return apperrors.NewValidation("暂无妙记") } return rt.CallMCP("get_minutes_basic_info", map[string]any{ "taskUuid": taskUUID, }) }, }
LatestMinutes: fetch the details of MY most recent minutes (听记) in one step.
Steps:
- list my minutes via list_by_keyword_and_time_range (belongingConditionId = "created"), optionally filtered by --keyword;
- pick the newest entry (largest create time, falling back to the first item) and read its taskUuid;
- print that minute's basic info via get_minutes_basic_info.
If the list is empty it reports "暂无妙记" instead of failing obscurely.
dws minutes +latest-minutes dws minutes +latest-minutes --keyword 周会
var ListTables = shortcut.Shortcut{ Service: "aitable", Command: "+list-tables", Product: "aitable", Description: "列出某个多维表(base)里的所有数据表(只读,投影 tableId/tableName)", Intent: "当你已经知道某个多维表(base)的 baseId、想一步看清这个 base 下都有哪些数据表(table)、" + "拿到它们的 tableId 和 tableName 以便后续查记录或改结构,却不想手动翻 base get 的完整目录时使用;" + "内部直接调用 get_tables,只传 baseId(不带 tableIds,因此返回该 base 下的全部数据表)," + "再在本地把每张表投影成 tableId、tableName 两个关键字段打印出来。" + "这是纯只读操作,只做列举与本地投影,不会创建、修改或删除任何表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "base", Type: shortcut.FlagString, Desc: "Base ID(要列出数据表的多维表)", Required: true}, }, Tips: []string{ `dws aitable +list-tables --base B`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("aitable", "get_tables", map[string]any{ "baseId": rt.Str("base"), }) if err != nil { return err } items := listTablesItems(data) if len(items) == 0 { return rt.Output(data) } results := make([]map[string]any, 0, len(items)) for _, t := range items { results = append(results, map[string]any{ "tableId": listTablesID(t), "tableName": listTablesName(t), }) } return rt.Output(map[string]any{"tables": results}) }, }
ListTables: list every data table (数据表) inside one multi-dimensional table (base) in a single step.
This is a read-only convenience wrapper over get_tables. It mirrors helpers tableGetCmd exactly: it calls the "aitable" server tool "get_tables" with a single argument, baseId ← --base (no tableIds → the server returns all tables in the base). The response's table list is then defensively projected down to {tableId, tableName} and printed via rt.Output so it honours --format/--jq/ --fields. When no recognisable table list is found it falls back to printing the raw payload.
dws aitable +list-tables --base B
var Lookup = shortcut.Shortcut{ Service: "contact", Command: "+lookup", Product: "contact", Description: "按姓名查询某人的完整资料(自动解析 userId 后取详情)", Intent: "当你只知道对方姓名、想一步拿到其完整资料(部门、职位、联系方式等)而不想先搜 userId 再查详情时使用;" + "内部先按姓名搜通讯录解析出唯一 userId,再取详情,姓名匹配到多人时会列出候选让你区分。只读操作。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "姓名/花名", Required: true}, }, Tips: []string{`dws contact +lookup --name 张三`}, Execute: func(rt *shortcut.RuntimeContext) error { user, err := resolveUser(rt, rt.Str("name")) if err != nil { return err } return rt.CallMCP("get_user_info_by_user_ids", map[string]any{ "user_id_list": []string{user.userID}, }) }, }
Lookup: resolve a person by NAME and return their full profile in one step.
Steps: search contacts by name → resolve to a single userId (disambiguate on multiple matches) → fetch full detail. Replaces `contact +search-user` (copy userId) → `contact +get-user --ids <id>`.
dws contact +lookup --name 张三
var MinutesDetail = shortcut.Shortcut{ Service: "minutes", Command: "+detail", Product: "minutes", Description: "一条命令聚合取一条妙记(听记)的多项产物(基础信息/摘要/关键词/逐字稿/待办)", Intent: "当你已经有某条听记的 taskUuid,想在一次操作里同时拿到它的基础信息、AI 摘要、关键词、逐字稿和待办,而不想分别敲 4~5 个子命令再自己拼时使用;" + "内部按 --artifacts 选择要拉的产物(默认全部:basic/summary/keywords/transcript/todos),逐个调用对应的原子工具并聚合成一个结果," + "某一项失败不会中断整体(会以错误字符串记录在该项下)。这是纯只读操作,不会修改听记;--direction 仅影响逐字稿排序(0=正序默认,1=倒序)。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "id", Type: shortcut.FlagString, Desc: "听记 taskUuid(必填)", Required: true}, {Name: "artifacts", Type: shortcut.FlagStringSlice, Desc: "要拉取的产物子集(默认全部)", Required: false, Enum: []string{"basic", "summary", "keywords", "transcript", "todos"}}, {Name: "direction", Type: shortcut.FlagString, Desc: "逐字稿排序: 0=正序(默认), 1=倒序(可选)", Required: false, Enum: []string{"0", "1"}}, }, Tips: []string{ `dws minutes +detail --id <taskUuid>`, `dws minutes +detail --id <taskUuid> --artifacts summary,todos`, `dws minutes +detail --id <taskUuid> --direction 1`, }, Execute: func(rt *shortcut.RuntimeContext) error { taskUUID := rt.Str("id") direction := rt.Str("direction") if direction == "" { direction = "0" } want := rt.StrSlice("artifacts") if len(want) == 0 { want = minutesArtifactOrder } bundle := map[string]any{"taskUuid": taskUUID} for _, raw := range want { name := strings.ToLower(strings.TrimSpace(raw)) tool, ok := minutesArtifactTools[name] if !ok { continue } params := map[string]any{"taskUuid": taskUUID} if name == "transcript" { params["direction"] = direction } data, err := rt.CallMCPData("minutes", tool, params) if err != nil { bundle[name] = map[string]any{"error": err.Error()} continue } bundle[name] = data } return rt.Output(bundle) }, }
MinutesDetail: fetch several artifacts of ONE minute (听记) in a single command and print them as one projected bundle.
dws exposes each artifact as its own atomic tool (get_minutes_basic_info / get_minutes_ai_summary / get_minutes_keywords / get_minutes_transcription / list_minutes_todos). To assemble a full picture a user otherwise has to call 4–5 commands and stitch the taskUuid through each. This shortcut fans them out for one taskUuid, tolerates partial failure (a failing artifact is recorded as an error string rather than aborting the whole bundle) and projects the result through rt.Output so it honours --format/--jq/--fields.
--artifacts selects which artifacts to pull (default: all). Each tool's params mirror the helper call sites in internal/helpers/minutes.go: every one takes a single "taskUuid", and transcription additionally takes "direction".
dws minutes +detail --id <taskUuid> dws minutes +detail --id <taskUuid> --artifacts summary,todos dws minutes +detail --id <taskUuid> --direction 1
var MinutesSearch = shortcut.Shortcut{ Service: "minutes", Command: "+minutes-search", Product: "minutes", Description: "按关键词搜索我的妙记并投影列表", Intent: "当你想按关键词快速找回自己创建的会议听记(妙记),只需要看到匹配到的标题、创建时间和 taskUuid 列表、而不想拿到一大坨原始字段时使用;" + "内部按 --query 关键词列出你创建的听记(最多 20 条),再在本地投影出每条的标题、创建时间和 taskUuid。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何听记;若没有匹配的听记则提示「没搜到妙记」。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "按关键词搜索听记(必填)", Required: true}, }, Tips: []string{ `dws minutes +minutes-search --query 周会`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("minutes", "list_by_keyword_and_time_range", map[string]any{ "belongingConditionId": "created", "maxResults": float64(20), "keyword": rt.Str("query"), }) if err != nil { return err } items := latestMinutesItems(data) results := make([]map[string]any, 0, len(items)) for _, m := range items { results = append(results, map[string]any{ "title": minutesSearchTitle(m), "createTime": minutesSearchCreateTime(m), "taskUuid": latestMinutesUUID(m), }) } if len(results) == 0 { return apperrors.NewValidation("没搜到妙记") } return rt.Output(map[string]any{"minutes": results}) }, }
MinutesSearch: search MY minutes (听记) by keyword and print a projected list.
Steps:
- list my minutes via list_by_keyword_and_time_range with belongingConditionId="created", maxResults=20 and keyword=--query, mirroring helpers.callListByKeywordRange;
- project each entry to {title, createTime, taskUuid} — field parsing is defensive (multiple candidate keys) — and print via rt.Output so it honours --format/--jq/--fields;
- if nothing matched, report "没搜到妙记" instead of an empty raw dump.
Read-only: it only lists and reshapes, never mutates any minute.
dws minutes +minutes-search --query 周会
var MyAttendance = shortcut.Shortcut{ Service: "attendance", Command: "+my-attendance", Product: "attendance", Description: "查我今天的考勤打卡记录(打卡流水,自动解析当前用户)", Intent: "当你想快速看自己今天的打卡流水(几点上下班打卡、打卡地址/定位方式)、又不想先查自己的 userId " + "再手动填写今天的起止时间时使用;内部先取当前登录用户的 userId,再按本地时区算出今天 00:00 到次日 00:00 的时间窗," + "最后查询你今天的打卡流水记录。只读操作,不会修改任何考勤数据;今天若还没有任何打卡则返回空结果。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{}, Tips: []string{ `dws attendance +my-attendance`, }, Execute: func(rt *shortcut.RuntimeContext) error { profile, err := rt.CallMCPData("contact", "get_current_user_profile", nil) if err != nil { return err } userID := myAttendanceCurrentUserID(profile) if userID == "" { return apperrors.NewValidation( "没能解析出当前登录用户的 userId,无法查询你的打卡记录;请确认已登录后重试。") } now := time.Now() start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) end := start.AddDate(0, 0, 1) const layout = "2006-01-02 15:04:05" data, err := rt.CallMCPData("attendance-wukong", "query_check_record", map[string]any{ "QueryCheckRecordRequest": map[string]any{ "userIds": []string{userID}, "checkDateFrom": start.Format(layout), "checkDateTo": end.Format(layout), }, }) if err != nil { return err } return rt.Output(data) }, }
MyAttendance: show MY punch-in (打卡流水) records for TODAY in one step.
Steps:
- resolve the current logged-in user's userId via the contact server's zero-arg get_current_user_profile (no --name needed — it's always "me");
- compute today's window [00:00 today, 00:00 tomorrow) in the local timezone and format both bounds as "yyyy-MM-dd HH:mm:ss" (the format the query_check_record helper feeds the tool);
- call query_check_record on the attendance-wukong server with the exact nested QueryCheckRecordRequest shape used by `dws attendance check record`, then print via rt.Output.
This replaces the manual dance of looking up your own userId, then running `dws attendance check record --users <id> --start <today> --end <today>`. Read-only; it never modifies any attendance data.
dws attendance +my-attendance
var MyFree = shortcut.Shortcut{ Service: "calendar", Command: "+my-free", Product: "calendar", Description: "查我自己在某时间段的忙闲(默认今天,无需输入姓名)", Intent: "当你(或 AI agent)想知道『我自己什么时候有空/忙』、用于安排会议或回复邀约时使用;" + "不用像 +free 那样传别人的姓名——内部自动解析当前用户的 userId,再查其忙闲时段。" + "默认查今天(本地时区 00:00 到次日 00:00),也可用 --start/--end 指定 ISO8601 时间范围。" + "只读操作,只查忙闲、不创建或修改任何日程;返回按时间排列的忙碌时段,空则表示这段时间全空。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "start", Type: shortcut.FlagString, Desc: "开始时间(ISO8601,可选,默认今天 00:00)", Required: false}, {Name: "end", Type: shortcut.FlagString, Desc: "结束时间(ISO8601,可选,默认次日 00:00)", Required: false}, }, Tips: []string{ `dws calendar +my-free`, `dws calendar +my-free --start 2026-07-10T09:00:00+08:00 --end 2026-07-10T18:00:00+08:00`, }, Execute: func(rt *shortcut.RuntimeContext) error { profile, err := rt.CallMCPData("contact", "get_current_user_profile", nil) if err != nil { return err } userID := myAttendanceCurrentUserID(profile) if userID == "" { return apperrors.NewValidation("无法解析当前用户的 userId") } now := time.Now() startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) startMillis := startOfToday.UnixMilli() endMillis := startOfToday.AddDate(0, 0, 1).UnixMilli() if rt.Str("start") != "" { ms, perr := freebusyParseMillis("start", rt.Str("start")) if perr != nil { return perr } startMillis = ms } if rt.Str("end") != "" { ms, perr := freebusyParseMillis("end", rt.Str("end")) if perr != nil { return perr } endMillis = ms } if endMillis <= startMillis { return apperrors.NewValidation("--end 必须晚于 --start") } data, err := rt.CallMCPData("calendar", "query_busy_status", map[string]any{ "startTime": startMillis, "endTime": endMillis, "userIds": []string{userID}, }) if err != nil { return err } busy := freebusySlots(data) return rt.Output(map[string]any{ "userId": userID, "busy": busy, "free": len(busy) == 0, }) }, }
MyFree: show MY own busy slots over a range — the self version of +free that needs no --who (you rarely want to type your own name). Defaults to today.
Steps:
resolve my own userId via the zero-arg get_current_user_profile (reusing myAttendanceCurrentUserID);
query_busy_status for [start,end] (defaults to today 00:00→tomorrow 00:00 in local time), then project result[].scheduleItems[] to a flat {start,end} busy list (reusing freebusySlots).
dws calendar +my-free dws calendar +my-free --start 2026-07-10T09:00:00+08:00 --end 2026-07-10T18:00:00+08:00
var MyGroups = shortcut.Shortcut{ Service: "chat", Command: "+my-groups", Product: "chat", Description: "列出我加入的群,可按类型过滤并投影关键字段", Intent: "当你想快速看一眼自己都加入了哪些群、以及每个群的会话ID、名称、群主和人数,而不想翻分页或盯着原始返回时使用;" + "内部分页拉取你加入的群列表,把每个群防御式地投影成 会话id / 名称 / 群主 / 人数 / 类型 等关键字段,输出成干净的结果。" + "可选 --type 在本地按群类型过滤(底层接口本身不带类型参数,故为客户端过滤)。这是只读操作,不会改动任何群或成员关系。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "type", Type: shortcut.FlagString, Desc: "按群类型过滤(可选,如返回中的 groupType/conversationType,大小写不敏感)", Required: false}, }, Tips: []string{ `dws chat +my-groups`, `dws chat +my-groups --type group`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("im", "list_my_groups_pagination", map[string]any{ "limit": 200, }) if err != nil { return err } groups := myGroupsExtract(data) typeFilter := strings.TrimSpace(rt.Str("type")) projected := make([]map[string]any, 0, len(groups)) for _, g := range groups { row := myGroupsProject(g) if typeFilter != "" { gt, _ := row["type"].(string) if !strings.EqualFold(strings.TrimSpace(gt), typeFilter) { continue } } projected = append(projected, row) } return rt.Output(map[string]any{ "count": len(projected), "groups": projected, }) }, }
MyGroups: list the groups I've joined and project just the key fields (会话id / 名称 / 群主 / 人数 / 类型) into a clean, composed payload — instead of paging through `chat group list-all` and squinting at the raw MCP response.
Steps:
- list my groups via list_my_groups_pagination (im server); param `limit` is copied verbatim from chat.go's `chat group list-all` call site.
- defensively project each group's key fields (field names probed across several candidate keys, since the gateway shape isn't guaranteed);
- optionally keep only groups whose type matches --type (Go-side filter — the underlying tool has no server-side type parameter).
Read-only: it never modifies any group or membership.
dws chat +my-groups dws chat +my-groups --type group
var MyInitiated = shortcut.Shortcut{ Service: "oa", Command: "+my-initiated", Product: "oa", Description: "列出我发起(提交)的审批单据", Intent: "当你想快速看清自己发起(提交)过哪些 OA 审批单据、方便跟进它们的进展时使用;" + "内部直接拉取当前用户已发起的审批实例列表(等价于 dws oa approval list-submitted)," + "再在本地把每条单据投影成标题、单号(businessId)、状态和审批实例 ID(processInstanceId) 四个关键字段。" + "可用 --query 按关键字过滤、--page/--limit 翻页(默认第 1 页、每页 20 条)。" + "这是纯只读操作,只做列表与本地投影,不会同意、拒绝、撤销或修改任何审批单据;若没有发起过审批则返回空列表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "关键字搜索(可选)", Required: false}, {Name: "page", Type: shortcut.FlagInt, Desc: "分页页码(可选,默认 1)", Default: "1", Required: false}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页大小(可选,默认 20)", Default: "20", Required: false}, }, Tips: []string{ `dws oa +my-initiated`, `dws oa +my-initiated --query 报销`, `dws oa +my-initiated --page 2 --limit 50`, }, Execute: func(rt *shortcut.RuntimeContext) error { page := rt.Int("page") if page <= 0 { page = 1 } limit := rt.Int("limit") if limit <= 0 { limit = 20 } params := map[string]any{ "pageNumber": float64(page), "pageSize": float64(limit), } if q := strings.TrimSpace(rt.Str("query")); q != "" { params["query"] = q } data, err := rt.CallMCPData("oa", "get_submitted_instances", params) if err != nil { return err } items := myInitiatedItems(data) if len(items) == 0 { return rt.Output(data) } results := make([]map[string]any, 0, len(items)) for _, m := range items { results = append(results, map[string]any{ "title": myInitiatedTitle(m), "businessId": myInitiatedBusinessID(m), "status": myInitiatedStatus(m), "processInstanceId": myInitiatedInstanceID(m), }) } return rt.Output(map[string]any{"initiated": results}) }, }
MyInitiated: list the OA approval instances *I* have initiated (submitted), in one step.
Steps:
call get_submitted_instances on the OA server — the exact tool + parameter names (pageNumber / pageSize as float64, optional query) used by helpers.approvalSubmittedListCmd ("dws oa approval list-submitted"). page and limit default to the same first-page values (page 1, limit 20).
defensively locate the instance list inside the response (probing common container keys, incl. a nested result/data object) and project each item down to {title, businessId, status, processInstanceId} with multiple candidate keys per field. When no recognisable list is found we print the raw payload so nothing is silently lost.
print via rt.Output so it honours --format / --jq / --fields.
Read-only: it only lists and reshapes my submitted approvals, it never approves, rejects, revokes or mutates anything.
dws oa +my-initiated dws oa +my-initiated --query 报销 dws oa +my-initiated --page 2 --limit 50
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 { 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 天无日程"}) } return rt.Output(map[string]any{"event": shortcutNextEventProject(event)}) }, }
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 Org = shortcut.Shortcut{ Service: "contact", Command: "+org", Product: "contact", Description: "按姓名查某人所在部门的详情(自动解析 userId 与 deptId)", Intent: "当你只知道某位同事的姓名、想知道 TA 所在部门(部门ID、名称、人数)时使用;" + "内部先按姓名解析出唯一 userId,再取 TA 的组织信息拿到主部门 deptId," + "最后打印该部门的详情。只读,不做任何修改。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "同事姓名/花名", Required: true}, }, Tips: []string{`dws contact +org --name 张三`}, Execute: func(rt *shortcut.RuntimeContext) error { user, err := resolveUser(rt, rt.Str("name")) if err != nil { return err } data, err := rt.CallMCPData("contact", "get_user_info_by_user_ids", map[string]any{ "user_id_list": []string{user.userID}, }) if err != nil { return err } deptID, ok := shortcutOrgExtractDeptID(data) if !ok { return apperrors.NewValidation(fmt.Sprintf( "没能从 %s(%s) 的组织信息里解析出所在部门 deptId;"+ "TA 可能没有归属部门,或返回结构与预期不符。", user.name, user.userID)) } return rt.CallMCP("get_dept_info_by_dept_id", map[string]any{ "deptId": deptID, }) }, }
Org: look up the department a person belongs to, by NAME, in one command.
Steps: resolve the person's name → userId → fetch their org detail (get_user_info_by_user_ids) → parse the primary deptId out of orgEmployeeModel.depts → print that department's detail (get_dept_info_by_dept_id). Replaces the manual dance of `contact user search` → copy userId → `contact user get --ids <id>` → copy deptId → `contact dept get-info --dept <deptId>`.
dws contact +org --name 张三
var Overdue = shortcut.Shortcut{ Service: "todo", Command: "+overdue", Product: "todo", Description: "列出我已过期未完成的待办", Intent: "当你想快速看清自己有哪些待办已经过了截止时间却还没做完、方便优先处理时使用;" + "内部先拉取你当前组织下作为执行人(executor)的待办列表,再在本地按「有截止时间(dueTime) 且早于当前时刻 且尚未完成」的条件筛选," + "最后只打印这些逾期待办的标题(subject)、截止时间(dueTime) 和任务 ID(taskId)。" + "这是纯只读操作,只做列表与本地过滤,不会修改或完成任何待办;若没有逾期待办则返回空列表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{}, Tips: []string{ `dws todo +overdue`, }, Execute: func(rt *shortcut.RuntimeContext) error { cards, err := shortcutListAllTodoCards(rt, map[string]any{ "roleTypes": []string{"executor"}, }) if err != nil { return err } now := time.Now().UnixMilli() overdue := make([]map[string]any, 0, len(cards)) for _, m := range cards { due, ok := shortcutOverdueDueTime(m) if !ok || due >= now { continue } if shortcutOverdueIsDone(m) { continue } taskID := shortcutTodoTaskID(m) subject, _ := m["subject"].(string) overdue = append(overdue, map[string]any{ "subject": subject, "dueTime": due, "taskId": taskID, }) } return rt.Output(map[string]any{"overdue": overdue}) }, }
Overdue: list MY overdue-and-unfinished todos in one step.
Steps:
list my todos via get_user_todos_in_current_org (pageNum / pageSize as strings, roleTypes=["executor"], mirroring helpers.todo list);
in Go, keep only the cards whose dueTime exists and is strictly before time.Now().UnixMilli() AND that are not yet done (isDone not true and finalStatusStage not a completed marker) — field parsing is defensive;
project each surviving card to {subject, dueTime, taskId} and print the list via rt.Output so it honours --format/--jq/--fields.
Read-only: it never mutates any todo, it only lists and filters locally.
dws todo +overdue
var PendingApprovals = shortcut.Shortcut{ Service: "oa", Command: "+pending", Product: "oa", Description: "只读列出待我审批的审批任务并投影为可读列表(只看不批)", Intent: "当你只想快速看一眼「待我审批」的审批任务清单——每条的标题、发起人、审批实例 ID 和创建时间——" + "而不想拿到一大坨原始字段时使用;内部拉取你近三个月待处理的审批单,再在本地投影出可读字段。" + "这是纯只读操作,只做列出与本地投影,绝不会同意、拒绝或以任何方式提交/修改审批(要一键通过请改用 `dws oa +approve-by`);" + "若当前没有待你审批的任务则提示「当前没有待我审批的任务」。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "limit", Type: shortcut.FlagInt, Desc: "最多列出多少条(可选)", Required: false}, }, Tips: []string{ `dws oa +pending`, `dws oa +pending --limit 10`, }, Execute: func(rt *shortcut.RuntimeContext) error { now := time.Now() params := map[string]any{ "starTime": float64(now.AddDate(0, 0, -90).UnixMilli()), "endTime": float64(now.UnixMilli()), } if rt.Changed("limit") { if n := rt.Int("limit"); n > 0 { params["pageSize"] = float64(n) } } data, err := rt.CallMCPData("oa", "list_pending_approvals", params) if err != nil { return err } items := shortcutApproveItems(data) results := make([]map[string]any, 0, len(items)) for _, m := range items { results = append(results, map[string]any{ "title": shortcutApproveTitle(m), "processInstanceId": shortcutApproveInstanceID(m), "originatorName": pendingApprovalsOriginator(m), "createTime": pendingApprovalsCreateTime(m), }) } if len(results) == 0 { return apperrors.NewValidation("当前没有待我审批的任务") } return rt.Output(map[string]any{"count": len(results), "pending": results}) }, }
PendingApprovals: READ-ONLY list of the approvals waiting for ME, projected to clean fields. Unlike `oa +approve-by` (which actually agrees to an approval), this shortcut only looks — it never approves / rejects / mutates anything.
Steps (tool name + param keys copied verbatim from helpers/oa.go):
list my pending approvals via list_pending_approvals with starTime / endTime as float64 milliseconds over a recent (~90 day) window, mirroring `dws oa approval list-pending`; optional --limit maps to pageSize (float64).
defensively unwrap the returned instance list (multiple candidate container keys, one nested level) and project each entry to a readable shape {title, originatorName, processInstanceId, createTime} — every field probed across several candidate keys.
if nothing is pending, report "当前没有待我审批的任务" instead of an empty raw dump.
dws oa +pending dws oa +pending --limit 10
var RecentMail = shortcut.Shortcut{ Service: "mail", Command: "+recent-mail", Product: "mail", Description: "列出收件箱近期邮件会话并投影列表(主题/发件人/时间/threadId)", Intent: "当你想快速看一眼自己邮箱里近期的邮件会话(收件箱线程/conversation),只需要一份精简清单(主题、发件人、最后修改时间、会话 threadId)、" + "而不想翻完整正文或原始字段时使用;" + "内部先确定要看的邮箱地址——你可以用 --email 指定,不指定时自动取你绑定的第一个邮箱——再解析要看的文件夹——" + "你可以用 --folder 指定文件夹 ID,不指定时自动定位收件箱——然后列出该文件夹下的近期会话," + "最后在本地把每条会话投影成 {subject, from, date, threadId} 打印出来,可配合 --format/--jq/--fields。" + "这是纯只读操作,只做列举与本地投影,不会修改、发送或删除任何邮件;若最近没有邮件则提示「最近没有邮件」。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "limit", Type: shortcut.FlagInt, Desc: "返回会话条数上限(可选,默认 20,最大 100)", Required: false}, {Name: "email", Type: shortcut.FlagString, Desc: "要查看的邮箱地址(可选,默认取你绑定的第一个邮箱)", Required: false}, {Name: "folder", Type: shortcut.FlagString, Desc: "文件夹 ID(可选,默认定位收件箱)", Required: false}, }, Tips: []string{ `dws mail +recent-mail`, `dws mail +recent-mail --limit 30`, `dws mail +recent-mail --email user@company.com --folder 2`, }, Execute: func(rt *shortcut.RuntimeContext) error { email := rt.Str("email") if email == "" { resolved, err := searchMailFirstMailbox(rt) if err != nil { return err } email = resolved } folderID := rt.Str("folder") if folderID == "" { resolved, err := recentMailInboxFolder(rt, email) if err != nil { return err } folderID = resolved } size := rt.Int("limit") if size <= 0 { size = 20 } if size > 100 { size = 100 } data, err := rt.CallMCPData("mail", "list_mailbox_threads", map[string]any{ "email": email, "folderId": folderID, "size": size, }) if err != nil { return err } threads := searchMailUnwrapList(data, "conversations", "threads", "result", "data", "list", "items", "records") results := make([]map[string]any, 0, len(threads)) for _, t := range threads { results = append(results, map[string]any{ "subject": searchMailFirstString(t, "subject", "title", "topic"), "from": recentMailSenders(t), "date": searchMailFirstAny(t, "lastModifiedDateTime", "date", "sentTime", "sentDate", "receivedDate", "createTime"), "threadId": searchMailFirstString(t, "threadId", "id", "conversationId"), }) } if len(results) == 0 { return apperrors.NewValidation("最近没有邮件") } return rt.Output(map[string]any{"count": len(results), "mails": results}) }, }
RecentMail: list recent inbox mail threads (conversations) and project a compact list in one step.
Steps:
resolve the mailbox address — use --email when given, otherwise pick the current user's first bound mailbox via list_user_mailboxes (reusing searchMailFirstMailbox from search_mail.go);
resolve the folder ID — list_mailbox_threads requires a folderId (a folder ID, not a name). Use --folder when given, otherwise resolve the inbox by listing the mailbox's top-level folders via list_folders and matching the inbox displayName (收件箱 / Inbox);
list the folder's threads via list_mailbox_threads (email / folderId / size mirror the helpers.mail thread-list call; size defaults to 20, capped at 100, matching the helper's 1..100 limit);
in Go, project each conversation to {subject, from, date, threadId} and print the list via rt.Output so it honours --format/--jq/--fields.
Read-only: it only lists and projects, never mutating any mail.
dws mail +recent-mail dws mail +recent-mail --limit 30 dws mail +recent-mail --email user@company.com --folder 2
Service: "aitable", Command: "+record-share-links", Product: "aitable", Description: "批量(可 >20 条)获取多维表记录分享链接:去重+分片+合并", Intent: "当你要一次性拿到很多条多维表记录的分享链接、数量可能超过底层工具单次 20 条上限时使用;" + "内部先对 --record-ids 去重(保持顺序),再按每批 ≤20 条切片,逐批调用 get_record_share_url(在 aitable-helper 服务上)," + "最后把各批返回的 {recordId, shareUrl} 合并成一个列表;某一批失败会记录错误但不影响其余批。" + "这是只读操作,只生成/获取分享链接、不修改记录。可选 --view-id 生成带视图上下文的链接。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "base", Type: shortcut.FlagString, Desc: "Base ID(记录所属 base)", Required: true}, {Name: "table", Type: shortcut.FlagString, Desc: "Table ID(记录所属数据表)", Required: true}, {Name: "record-ids", Type: shortcut.FlagStringSlice, Desc: "记录 ID 列表,可 >20(自动去重+分片,必填)", Required: true}, {Name: "view-id", Type: shortcut.FlagString, Desc: "视图 ID:生成带视图上下文的链接(可选)", Required: false}, }, Tips: []string{ `dws aitable +record-share-links --base B --table T --record-ids rec1,rec2,rec3`, `dws aitable +record-share-links --base B --table T --record-ids rec1 --view-id viw_VIP`, }, Execute: func(rt *shortcut.RuntimeContext) error { ids := dedupStrings(rt.StrSlice("record-ids")) if len(ids) == 0 { return apperrors.NewValidation("--record-ids 去重后为空") } baseID := rt.Str("base") tableID := rt.Str("table") viewID := rt.Str("view-id") items := make([]map[string]any, 0, len(ids)) var batchErrors []map[string]any for start := 0; start < len(ids); start += recordShareBatchSize { end := start + recordShareBatchSize if end > len(ids) { end = len(ids) } chunk := ids[start:end] params := map[string]any{ "baseId": baseID, "tableId": tableID, "recordIds": chunk, } if viewID != "" { params["viewId"] = viewID } data, err := rt.CallMCPData("aitable-helper", "get_record_share_url", params) if err != nil { batchErrors = append(batchErrors, map[string]any{ "recordIds": chunk, "error": err.Error(), }) continue } items = append(items, recordShareItems(data)...) } out := map[string]any{ "base": baseID, "table": tableID, "total": len(ids), "batches": (len(ids) + recordShareBatchSize - 1) / recordShareBatchSize, "items": items, } if len(batchErrors) > 0 { out["errors"] = batchErrors } return rt.Output(out) }, }
RecordShareLinks: get share links for MANY aitable (多维表) records in one command, transparently working around the tool's per-call cap.
The atomic tool get_record_share_url accepts a recordIds array but caps a single call at 20 (see the +record-share-url 1:1 shortcut and internal/helpers/aitable.go). To share more than 20 records a user has to split the list by hand and stitch the results. This shortcut dedups the requested recordIds (preserving order), chunks them into batches of ≤20, fans each batch out to get_record_share_url and merges every returned {recordId, shareUrl} into one projected list — a failing batch is recorded and does not abort the rest.
Cross-server note: get_record_share_url runs on the "aitable-helper" MCP server (not "aitable"), so the calls go through CallMCPData with an explicit product, mirroring helpers.callAitableHelperTool.
dws aitable +record-share-links --base B --table T --record-ids rec1,rec2,…,rec50 dws aitable +record-share-links --base B --table T --record-ids rec1 --view-id viw_VIP
var RelatedTasks = shortcut.Shortcut{ Service: "todo", Command: "+related-tasks", Product: "todo", Description: "一次性列出与我相关的全部待办(我作为创建人/执行人/参与人三种角色的并集,按 taskId 去重)", Intent: "当你想一次看清『所有和我有关的待办』——不管是我创建(creator)的、指派给我执行(executor)的、还是我作为参与人(participant)协作的——时使用;" + "内部默认拉取你当前组织下 roleTypes=[\"creator\",\"executor\",\"participant\"] 三种角色的待办并集(这三个值正是待办列表支持的角色枚举)," + "再在本地按任务 ID(taskId) 去重(同一条待办可能因多角色重复出现),把每条投影成标题、状态、优先级、创建人、计划完成时间和 taskId 打印出来。" + "可用 --role-types 以逗号分隔覆盖默认角色(取值 creator/executor/participant),可用 --status 透传 todoStatus 过滤状态。" + "这是纯只读操作,只做列表、去重与投影,不会创建或修改任何待办;若没有与你相关的待办则返回空列表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ { Name: "role-types", Type: shortcut.FlagString, Desc: "覆盖默认角色范围,逗号分隔,取值 creator/executor/participant;不传则默认三者并集", }, { Name: "status", Type: shortcut.FlagString, Desc: "按 todoStatus 过滤(透传给 get_user_todos_in_current_org)", }, }, Tips: []string{ `dws todo +related-tasks`, `dws todo +related-tasks --role-types creator,executor`, `dws todo +related-tasks --status TODO`, }, Execute: func(rt *shortcut.RuntimeContext) error { roleTypes := []string{"creator", "executor", "participant"} if rt.Changed("role-types") { parsed, err := parseRelatedRoleTypes(rt.Str("role-types")) if err != nil { return err } if len(parsed) > 0 { roleTypes = parsed } } params := map[string]any{ "roleTypes": roleTypes, } if rt.Changed("status") { if v := strings.TrimSpace(rt.Str("status")); v != "" { params["todoStatus"] = v } } cards, err := shortcutListAllTodoCards(rt, params) if err != nil { return err } seen := make(map[string]bool, len(cards)) results := make([]map[string]any, 0, len(cards)) for _, m := range cards { taskID := shortcutRelatedTaskID(m) if taskID != "" { if seen[taskID] { continue } seen[taskID] = true } results = append(results, shortcutRelatedProject(m, taskID)) } if len(results) == 0 { return apperrors.NewValidation("没有与你相关的待办(creator/executor/participant 三种角色下均为空)") } return rt.Output(map[string]any{"tasks": results, "count": len(results)}) }, }
RelatedTasks: list ALL todos related to me in one step — the union of the todos where I am the creator, the executor, or a participant — mirroring the creator+executor+participant roles-union view.
Steps:
list my todos via get_user_todos_in_current_org (pageNum / pageSize as strings, mirroring helpers.todo list) with roleTypes defaulting to all three enum values ["creator","executor","participant"], so the server returns todos where I hold ANY of those roles. creator / executor / participant are exactly the values accepted by helpers.parseRoleTypes. --role-types (CSV) can override the default; --status is passed through as todoStatus, both mirroring helpers.buildListTodoTaskArgs;
dedupe the returned cards by taskId (one todo can appear more than once when I hold several roles on it), then project each surviving card to a clean {title, status, priority, creator, planFinishDate, taskId} shape with defensive multi-key field probing;
print the deduped list via rt.Output so it honours --format/--jq/--fields.
Read-only: it only lists, dedupes and projects, it never mutates any todo.
dws todo +related-tasks
var Remind = shortcut.Shortcut{ Service: "todo", Command: "+remind", Product: "todo", Description: "给自己创建一条带截止/提醒时间的待办", Intent: "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;" + "内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "task", Type: shortcut.FlagString, Desc: "待办标题/内容", Required: true}, {Name: "at", Type: shortcut.FlagString, Desc: "截止/提醒时间(ISO8601,可选,如 2026-03-10T18:00:00+08:00)"}, }, Tips: []string{`dws todo +remind --task "交周报" --at 2026-03-10T18:00:00+08:00`}, Execute: func(rt *shortcut.RuntimeContext) error { profile, err := rt.CallMCPData("contact", "get_current_user_profile", nil) if err != nil { return err } userID := myAttendanceCurrentUserID(profile) if userID == "" { return apperrors.NewValidation("无法解析当前登录用户的 userId,无法给自己创建待办") } vo := map[string]any{ "subject": rt.Str("task"), "executorIds": []string{userID}, } if rt.Changed("at") { ms, err := shortcutRemindParseMillis("at", rt.Str("at")) if err != nil { return err } vo["dueTime"] = ms } return rt.CallMCP("create_personal_todo", map[string]any{ "PersonalTodoCreateVO": vo, }) }, }
Remind: create a personal todo for YOURSELF with an optional due/reminder time, in one command. It resolves the current user and explicitly passes executorIds; the real todo backend does not reliably default a missing executor to "me".
Steps: take --task as the subject, and (optionally) parse --at into epoch milliseconds (mirroring the todo helper's parseISOTimeToMillis, which stores dueTime as int64 millis) → create_personal_todo. Replaces having to look up your own userId before `todo +create`.
dws todo +remind --task "交周报" --at 2026-03-10T18:00:00+08:00
var ReplaceBatch = shortcut.Shortcut{ Service: "minutes", Command: "+replace-batch", Product: "minutes", Description: "对一条妙记(听记)批量执行多组文字替换(原文=>替换)", Intent: "当你要在同一条听记里一次性纠正多个词(如把多个错识别的人名/术语统一替换),而底层工具一次只能替换一组时使用;" + "内部按多个 --pair \"原文=>替换\" 逐组调用替换工具,先在本地校验去重(同一个「原文」不能出现两次,避免两条规则互相打架)," + "再逐组应用并聚合每组的成功/失败结果,某一组失败不会中断其余组。这是写操作,会实际修改听记文字内容,请确认 taskUuid 与替换规则无误。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "id", Type: shortcut.FlagString, Desc: "听记 taskUuid(必填)", Required: true}, {Name: "pair", Type: shortcut.FlagStringSlice, Desc: `替换规则,格式 "原文=>替换",可重复传多组(必填)`, Required: true}, }, Constraints: []shortcut.Constraint{ { Kind: shortcut.ConstraintCustom, Flags: []string{"pair"}, Description: `每个 --pair 必须使用 "原文=>替换" 格式,原文不能为空且不能重复`, }, }, Tips: []string{ `dws minutes +replace-batch --id <taskUuid> --pair "张三=>张三丰"`, `dws minutes +replace-batch --id <taskUuid> --pair "Q2=>第二季度" --pair "PM=>产品经理"`, }, Validate: func(rt *shortcut.RuntimeContext) error { _, err := parseReplacePairs(rt.StrSlice("pair")) return err }, Execute: func(rt *shortcut.RuntimeContext) error { taskUUID := rt.Str("id") pairs, err := parseReplacePairs(rt.StrSlice("pair")) if err != nil { return err } if rt.DryRun() { replacements := make([]map[string]any, 0, len(pairs)) for _, p := range pairs { replacements = append(replacements, map[string]any{ "originalText": p.orig, "replacedText": p.repl, }) } return rt.Output(map[string]any{ "dryRun": true, "taskUuid": taskUUID, "total": len(pairs), "replacements": replacements, }) } results := make([]map[string]any, 0, len(pairs)) applied := 0 for _, p := range pairs { _, callErr := rt.CallMCPWriteData("minutes", "replace_minutes_text", map[string]any{ "taskUuid": taskUUID, "originalText": p.orig, "replacedText": p.repl, }) entry := map[string]any{"originalText": p.orig, "replacedText": p.repl} if callErr != nil { entry["error"] = callErr.Error() } else { entry["applied"] = true applied++ } results = append(results, entry) } return rt.Output(map[string]any{ "taskUuid": taskUUID, "total": len(pairs), "applied": applied, "failed": len(pairs) - applied, "results": results, }) }, }
ReplaceBatch: apply MANY word replacements to one minute (听记) in a single command.
dws's atomic tool replace_minutes_text handles exactly one originalText→replacedText pair (see internal/helpers/minutes.go, the +word-replace 1:1 shortcut). Fixing several terms at once means calling it repeatedly and eyeballing each result. This shortcut takes multiple --pair "原文=>替换" entries, validates them (rejecting duplicate source words so two rules don't fight over the same term), applies each via replace_minutes_text and aggregates a per-pair {applied|error} report through rt.Output — one failing pair does not abort the rest.
dws minutes +replace-batch --id <taskUuid> --pair "张三=>张三丰" --pair "Q2=>第二季度"
var ReportLatest = shortcut.Shortcut{ Service: "report", Command: "+report-latest", Product: "report", Description: "取我最新提交的一篇日志/汇报详情(钉钉原生)", Intent: "当你只想快速看回自己最近发出的一篇日志/汇报,却不想先翻发件箱列表、复制 reportId 再查详情时使用;" + "内部先列出你近期发出的日志(默认最近 20 天,服务端单次查询跨度上限 20 天;可用 --keyword 按日志模板名过滤)," + "自动挑出创建时间最新的一条,再拉取它的正文详情(字段明细、发送人、时间、钉钉跳转链接等)。" + "这是只读操作,不会创建或修改任何日志;若这段时间内你没有发出过日志则提示「暂无日志」。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "keyword", Type: shortcut.FlagString, Desc: "按日志模板名过滤(可选,对应 report outbox list 的 --template-name)", Required: false}, }, Tips: []string{ `dws report +report-latest`, `dws report +report-latest --keyword 日报`, }, Execute: func(rt *shortcut.RuntimeContext) error { now := time.Now() startMs := now.AddDate(0, 0, -20).Truncate(24 * time.Hour).UnixMilli() endMs := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).UnixMilli() listArgs := map[string]any{ "cursor": float64(0), "size": float64(20), "startTime": float64(startMs), "endTime": float64(endMs), } if kw := strings.TrimSpace(rt.Str("keyword")); kw != "" { listArgs["report_template_name"] = kw } data, err := rt.CallMCPData("report", "get_send_report_list", listArgs) if err != nil { return err } row, reportID := shortcutReportLatestPick(data) if row == nil { return apperrors.NewValidation("暂无日志") } if reportID == "" { return rt.Output(row) } return rt.CallMCP("get_report_entry_details", map[string]any{ "report_id": reportID, }) }, }
ReportLatest: fetch the detail of MY most recently submitted report (日志/汇报) in one step (DingTalk-native).
Steps:
list the reports I sent via get_send_report_list. cursor/size/startTime/ endTime mirror helpers.runReportSent — the server caps a single query span at 20 days, so we default to the last-20-days window just like the helper. An optional --keyword is passed through as report_template_name (the same filter the helper exposes as --template-name).
locate the newest entry: the item with the largest create time (createTime/gmtCreate/sendTime), falling back to the first item that carries a reportId (lists come back newest-first).
print that report's body via get_report_entry_details (report_id param mirrors helpers.runReportDetail). If nothing carries a reportId we fall back to printing the picked list row via rt.Output.
If I have not sent any report in the window it reports "暂无日志" instead of failing obscurely.
dws report +report-latest dws report +report-latest --keyword 日报
var Reschedule = shortcut.Shortcut{ Service: "calendar", Command: "+reschedule", Product: "calendar", Description: "改一个已有日程的时间(只动开始/结束时间,其他字段不变)", Intent: "当你想把一个已经存在的日程改到新的时间段、又不想动标题/描述/参会人等其他内容时使用;" + "内部先用 eventId 拉一次日程详情确认它真实存在,再只更新开始和结束时间。" + "如果 eventId 查不到会直接报错,不会误改别的日程。" + "会真实修改该日程的时间。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "event", Type: shortcut.FlagString, Desc: "要改期的日程 eventId(可用 dws calendar event list 查询)", Required: true}, {Name: "start", Type: shortcut.FlagString, Desc: "新的开始时间(ISO8601,如 2026-03-10T15:00:00+08:00)", Required: true}, {Name: "end", Type: shortcut.FlagString, Desc: "新的结束时间(ISO8601,如 2026-03-10T16:00:00+08:00)", Required: true}, }, Tips: []string{ `dws calendar +reschedule --event EVENT_ID --start "2026-03-10T15:00:00+08:00" --end "2026-03-10T16:00:00+08:00"`, }, Execute: func(rt *shortcut.RuntimeContext) error { eventID := strings.TrimSpace(rt.Str("event")) if eventID == "" { return apperrors.NewValidation("--event 不能为空") } start := strings.TrimSpace(rt.Str("start")) end := strings.TrimSpace(rt.Str("end")) if start == "" || end == "" { return apperrors.NewValidation("--start 与 --end 都必须提供(ISO8601 时间字符串)") } if _, err := rt.CallMCPData("calendar", "get_calendar_detail", map[string]any{ "eventId": eventID, }); err != nil { return err } return rt.CallMCP("update_calendar_event", map[string]any{ "eventId": eventID, "startDateTime": start, "endDateTime": end, }) }, }
Reschedule: change the time of an EXISTING calendar event in one step, leaving every other field (title, description, attendees, rooms, ...) untouched.
Steps: confirm the event exists via get_calendar_detail (so a bad eventId fails clearly before any write), then update only its start/end time via update_calendar_event. Replaces `calendar event get --id` (verify) → `calendar event update --id --start --end` where you must remember not to touch anything else.
dws calendar +reschedule --event EVENT_ID \ --start "2026-03-10T15:00:00+08:00" --end "2026-03-10T16:00:00+08:00"
var ResolveBase = shortcut.Shortcut{ Service: "aitable", Command: "+resolve-base", Product: "aitable", Description: "按名称搜索多维表 Base 并解析出唯一 baseId(只读)", Intent: "当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;" + "内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。" + "如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "要搜索的 Base 名称关键词(必填)", Required: true}, }, Tips: []string{ `dws aitable +resolve-base --name 项目管理`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("aitable", "search_bases", map[string]any{ "query": rt.Str("name"), }) if err != nil { return err } items := resolveBaseItems(data) candidates := make([]map[string]any, 0, len(items)) for _, b := range items { candidates = append(candidates, map[string]any{ "baseId": resolveBaseID(b), "name": resolveBaseName(b), }) } switch len(candidates) { case 0: return apperrors.NewValidation("没有找到名称包含 " + rt.Str("name") + " 的 Base") case 1: return rt.Output(map[string]any{ "resolved": true, "baseId": candidates[0]["baseId"], "name": candidates[0]["name"], }) default: return rt.Output(map[string]any{ "resolved": false, "count": len(candidates), "candidates": candidates, }) } }, }
ResolveBase: resolve a 多维表 Base by name keyword into a single baseId.
This is the Base-level analogue of "resolve a user by name". It searches Bases by name and disambiguates:
- search Bases via search_bases (mirrors helpers base search, MCP arg "query" ← --name);
- project each candidate to {baseId, name} — field parsing is defensive (multiple candidate keys);
- exactly one match → return {resolved:true, baseId, name}; multiple matches → return {resolved:false, count, candidates} and let the caller pick (never guesses); zero matches → report a validation error instead of an empty raw dump.
Read-only: it only searches and reshapes, never mutates any Base.
dws aitable +resolve-base --name 项目管理
var ResolveDept = shortcut.Shortcut{ Service: "contact", Command: "+resolve-dept", Product: "contact", Description: "按名称搜索部门并解析出唯一 deptId(只读)", Intent: "当你只知道某个部门的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 deptId 时使用;" + "内部按 --name 关键词调用 search_dept_by_keyword 搜索部门,再在本地投影出每个候选的 deptId 和 name。" + "如果只命中一个部门就直接返回它的 deptId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何部门,也不会列出部门成员。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "要搜索的部门名称关键词(必填)", Required: true}, }, Tips: []string{ `dws contact +resolve-dept --name 技术部`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("contact", "search_dept_by_keyword", map[string]any{ "query": rt.Str("name"), }) if err != nil { return err } items := resolveDeptItems(data) candidates := make([]map[string]any, 0, len(items)) for _, d := range items { candidates = append(candidates, map[string]any{ "deptId": resolveDeptID(d), "name": resolveDeptName(d), }) } switch len(candidates) { case 0: return apperrors.NewValidation("没有找到名称包含 " + rt.Str("name") + " 的部门") case 1: return rt.Output(map[string]any{ "resolved": true, "deptId": candidates[0]["deptId"], "name": candidates[0]["name"], }) default: return rt.Output(map[string]any{ "resolved": false, "count": len(candidates), "candidates": candidates, }) } }, }
ResolveDept: resolve a department by name keyword into a single deptId.
This is the department-level analogue of "resolve a user by name". It searches departments by name and disambiguates:
- search departments via search_dept_by_keyword (mirrors helpers contact dept search, MCP arg "query" ← --name);
- project each candidate to {deptId, name} — field parsing is defensive (multiple candidate keys);
- exactly one match → return {resolved:true, deptId, name}; multiple matches → return {resolved:false, count, candidates} and let the caller pick (never guesses); zero matches → report a validation error instead of an empty raw dump.
Read-only: it only searches and reshapes, never mutates anything. Unlike +dept-members it stops at the deptId and does NOT list members.
dws contact +resolve-dept --name 技术部
var ResolveSpace = shortcut.Shortcut{ Service: "wiki", Command: "+resolve-space", Product: "wiki", Description: "按名称搜索知识空间并解析出唯一 spaceId(只读)", Intent: "当你只知道某个知识空间(wiki space)的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 spaceId 时使用;" + "内部按 --name 关键词调用 search_wikiSpaces 搜索知识空间,再在本地投影出每个候选的 spaceId 和 name。" + "如果只命中一个知识空间就直接返回它的 spaceId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何知识空间。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "要搜索的知识空间名称关键词(必填)", Required: true}, }, Tips: []string{ `dws wiki +resolve-space --name 产品文档`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("wiki", "search_wikiSpaces", map[string]any{ "keyword": rt.Str("name"), }) if err != nil { return err } items := resolveSpaceItems(data) candidates := make([]map[string]any, 0, len(items)) for _, s := range items { candidates = append(candidates, map[string]any{ "spaceId": resolveSpaceID(s), "name": resolveSpaceName(s), }) } switch len(candidates) { case 0: return apperrors.NewValidation("没有找到名称包含 " + rt.Str("name") + " 的知识空间") case 1: return rt.Output(map[string]any{ "resolved": true, "spaceId": candidates[0]["spaceId"], "name": candidates[0]["name"], }) default: return rt.Output(map[string]any{ "resolved": false, "count": len(candidates), "candidates": candidates, }) } }, }
ResolveSpace: resolve a wiki 知识空间 by name keyword into a single spaceId.
This is the wiki-space-level analogue of "resolve a user by name". It searches knowledge spaces by name and disambiguates:
- search spaces via search_wikiSpaces (mirrors helpers wiki space search, MCP arg "keyword" ← --name);
- project each candidate to {spaceId, name} — field parsing is defensive (multiple candidate keys);
- exactly one match → return {resolved:true, spaceId, name}; multiple matches → return {resolved:false, count, candidates} and let the caller pick (never guesses); zero matches → report a validation error instead of an empty raw dump.
Read-only: it only searches and reshapes, never mutates any space.
dws wiki +resolve-space --name 产品文档
var ResolveTable = shortcut.Shortcut{ Service: "aitable", Command: "+resolve-table", Product: "aitable", Description: "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)", Intent: "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、" + "想把它解析成可直接用于后续工具的 tableId 时使用;" + "内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name," + "并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。" + "如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。" + "这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "base", Type: shortcut.FlagString, Desc: "Base ID(要在其内解析数据表的多维表)", Required: true}, {Name: "name", Type: shortcut.FlagString, Desc: "要匹配的数据表名称关键词(必填)", Required: true}, }, Tips: []string{ `dws aitable +resolve-table --base B --name 任务`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("aitable", "get_tables", map[string]any{ "baseId": rt.Str("base"), }) if err != nil { return err } items := resolveTableItems(data) all := make([]map[string]any, 0, len(items)) for _, t := range items { all = append(all, map[string]any{ "tableId": resolveTableID(t), "name": resolveTableName(t), }) } needle := strings.ToLower(rt.Str("name")) candidates := make([]map[string]any, 0, len(all)) for _, c := range all { name, _ := c["name"].(string) if strings.Contains(strings.ToLower(name), needle) { candidates = append(candidates, c) } } switch len(candidates) { case 0: return apperrors.NewValidation("Base 内没有名称包含 " + rt.Str("name") + " 的数据表") case 1: return rt.Output(map[string]any{ "resolved": true, "tableId": candidates[0]["tableId"], "name": candidates[0]["name"], "base": rt.Str("base"), }) default: return rt.Output(map[string]any{ "resolved": false, "count": len(candidates), "candidates": candidates, }) } }, }
ResolveTable: resolve a 数据表 (table) inside one Base by name keyword into a single tableId.
This is the table-level analogue of "resolve a Base by name". Because there is no server tool that searches tables by name, it lists every table in the Base via get_tables (baseId ← --base, verbatim from list_tables / helpers tableGetCmd) and then matches --name locally:
- project each table to {tableId, name} — field parsing is defensive (multiple candidate keys);
- filter locally by a case-insensitive substring match on name;
- exactly one match → return {resolved:true, tableId, name, base}; multiple matches → return {resolved:false, count, candidates} and let the caller pick (never guesses); zero matches → report a validation error instead of an empty raw dump.
Read-only: it only lists and reshapes, never mutates any table.
dws aitable +resolve-table --base B --name 任务
var RespondEvent = shortcut.Shortcut{ Service: "calendar", Command: "+respond-event", Product: "calendar", Description: "接受 / 拒绝 / 暂定回复一个日程邀请(作为参会人设置自己的响应状态)", Intent: "当你收到一个日程邀请、想直接确认接受、婉拒或标记为暂定,而不想记忆 accepted/declined 这类过去式接口取值时使用;" + "你只需提供日程的 eventId(用 `dws calendar event list` 查询)和一个直白的动作 accept/decline/tentative," + "内部会把它映射为服务端的 accepted/declined/tentative 并调用日历的 respond 能力设置你的参会响应状态。" + "注意:这会真实修改你在该日程上的响应状态;订阅日历下的日程没有参会人,因此无法响应。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "event", Type: shortcut.FlagString, Desc: "日程 eventId(用 `dws calendar event list` 查询)", Required: true}, {Name: "response", Type: shortcut.FlagString, Desc: "响应动作:accept(接受) / decline(拒绝) / tentative(暂定)", Required: true, Enum: []string{"accept", "decline", "tentative"}}, }, Tips: []string{ `dws calendar +respond-event --event EVENT_ID --response accept`, `dws calendar +respond-event --event EVENT_ID --response decline`, `dws calendar +respond-event --event EVENT_ID --response tentative`, }, Execute: func(rt *shortcut.RuntimeContext) error { eventID := strings.TrimSpace(rt.Str("event")) if eventID == "" { return apperrors.NewValidation("请用 --event 提供日程的 eventId(可用 `dws calendar event list` 查询)") } // Map the ergonomic verb onto the tool's responseStatus wire value. // The `respond` tool accepts accepted/declined/tentative (see // helpers/calendar.go event respond); the Enum on --response already // guarantees one of accept/decline/tentative here. var status string switch strings.TrimSpace(strings.ToLower(rt.Str("response"))) { case "accept": status = "accepted" case "decline": status = "declined" case "tentative": status = "tentative" default: return apperrors.NewValidation("--response 只允许 accept / decline / tentative") } return rt.CallMCP("respond", map[string]any{ "eventId": eventID, "responseStatus": status, }) }, }
RespondEvent: accept / decline / tentatively respond to a calendar event invitation in one command, as the current user (the event attendee).
This wraps the `respond` MCP tool from helpers/calendar.go verbatim (mirroring `dws calendar event respond`): it takes eventId + responseStatus, where responseStatus is one of needsAction/accepted/declined/tentative. The shortcut exposes the ergonomic verbs accept/decline/tentative on --response and maps them onto the tool's accepted/declined/tentative values so the caller never has to remember the past-tense wire spelling.
dws calendar +respond-event --event EVENT_ID --response accept dws calendar +respond-event --event EVENT_ID --response decline dws calendar +respond-event --event EVENT_ID --response tentative
var SearchMail = shortcut.Shortcut{ Service: "mail", Command: "+search-mail", Product: "mail", Description: "按 KQL 关键词搜索邮件并投影列表(主题/发件人/时间/messageId)", Intent: "当你想按关键词(KQL 表达式,如 subject:周报、from:alice、hasAttachments:true、folderId:2 等)快速搜自己的邮件、" + "并只看一份精简清单(主题、发件人、时间、邮件 messageId)而不想翻完整正文时使用;" + "内部先确定要搜的邮箱地址——你可以用 --email 指定,不指定时自动取你绑定的第一个邮箱——再执行邮件搜索," + "最后在本地把每封邮件投影成 {subject, from, date, messageId} 打印出来,可配合 --format/--jq/--fields。" + "这是纯只读操作,只做搜索与本地投影,不会修改、发送或删除任何邮件;若没有命中则返回空列表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "KQL 搜索表达式(如 subject:周报、from:alice、folderId:2)", Required: true}, {Name: "email", Type: shortcut.FlagString, Desc: "要搜索的邮箱地址(可选,默认取你绑定的第一个邮箱)", Required: false}, {Name: "size", Type: shortcut.FlagString, Desc: "返回条数上限(可选,默认 20)", Required: false}, }, Tips: []string{ `dws mail +search-mail --query "subject:周报"`, `dws mail +search-mail --query "from:alice AND date>2025-06-01T00:00:00Z"`, `dws mail +search-mail --email user@company.com --query "hasAttachments:true"`, }, Execute: func(rt *shortcut.RuntimeContext) error { if err := rt.RequireAll("query"); err != nil { return err } email := rt.Str("email") if email == "" { resolved, err := searchMailFirstMailbox(rt) if err != nil { return err } email = resolved } size := rt.Str("size") if size == "" { size = "20" } data, err := rt.CallMCPData("mail", "search_emails", map[string]any{ "email": email, "query": rt.Str("query"), "size": size, }) if err != nil { return err } messages := searchMailMessages(data) out := make([]map[string]any, 0, len(messages)) for _, m := range messages { out = append(out, map[string]any{ "subject": searchMailFirstString(m, "subject", "title", "topic"), "from": searchMailFrom(m), "date": searchMailFirstAny(m, "date", "sentDate", "receivedDate", "sentTime", "internalDate", "createTime"), "messageId": searchMailFirstString(m, "messageId", "id", "mailId", "emailId", "internetMessageId"), }) } return rt.Output(map[string]any{"messages": out, "email": email}) }, }
SearchMail: search a mailbox by keyword and project a compact list in one step.
Steps:
resolve the mailbox address — use --email when given, otherwise pick the current user's first bound mailbox via list_user_mailboxes;
search that mailbox via search_emails (email / query / size mirror helpers.messageSearch; size defaults to "20" as a string, matching the helper's sizeVal handling);
in Go, project each returned message to {subject, from, date, messageId} and print the list via rt.Output so it honours --format/--jq/--fields.
Read-only: it only lists and projects, never mutating any mail.
dws mail +search-mail --query "subject:周报" dws mail +search-mail --query "from:alice AND date>2025-06-01T00:00:00Z" dws mail +search-mail --email user@company.com --query "hasAttachments:true"
var SearchMsg = shortcut.Shortcut{ Service: "chat", Command: "+search-msg", Product: "chat", Description: "在群内按关键词搜消息(默认近 7 天,投影发送人/时间/内容/messageId)", Intent: "当你想在某个群里按关键词翻最近的聊天记录、但不想手动把起止时间换算成毫秒、也不想记 message search 的一堆参数时使用;" + "用 --group 指定群的 openConversationId、--query 指定搜索关键词,内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗," + "搜索这段时间内该群里命中关键词的消息,再在本地把每条消息投影成发送人、时间、内容、messageId 四个关键字段。" + "这是纯只读操作,只做搜索与本地投影,不会发送、撤回或标记任何消息。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群会话的 openConversationId(必填)"}, {Name: "conversation-id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true}, {Name: "id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true}, {Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词(必填)"}, {Name: "keyword", Type: shortcut.FlagString, Desc: "--query 的别名", Hidden: true}, {Name: "days", Type: shortcut.FlagInt, Desc: "回溯天数(可选,默认 7)", Default: "7", Required: false}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id", "id"}}, {Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"query", "keyword"}}, }, Tips: []string{ `dws chat +search-msg --group <openConversationId> --query "changefree"`, `dws chat +search-msg --group <openConversationId> --query "周报" --days 3`, }, Execute: func(rt *shortcut.RuntimeContext) error { group := rt.StrFirst("group", "conversation-id", "id") query := rt.StrFirst("query", "keyword") days := rt.Int("days") if days <= 0 { days = 7 } now := time.Now() startMs := now.AddDate(0, 0, -days).UnixMilli() endMs := now.UnixMilli() data, err := rt.CallMCPData("chat", "search_messages_by_keyword", map[string]any{ "keyword": query, "startTime": startMs, "endTime": endMs, "limit": 100, "cursor": "0", "openConversationId": group, }) if err != nil { return err } items := searchMsgItems(data) if len(items) == 0 { return rt.Output(data) } results := make([]map[string]any, 0, len(items)) for _, m := range items { results = append(results, map[string]any{ "sender": searchMsgSender(m), "time": searchMsgTime(m), "text": searchMsgText(m), "messageId": searchMsgMessageID(m), }) } return rt.Output(map[string]any{"messages": results}) }, }
SearchMsg: search messages inside a single group chat by keyword in one step.
Steps:
- compute the look-back window [now-Nd, now] in local time and express both bounds as epoch millis. N defaults to 7 days and is overridable via --days; mirroring `dws chat message search`, which feeds startTime/endTime as epoch millis to search_messages_by_keyword.
- call search_messages_by_keyword on the chat server with keyword/startTime/ endTime/limit/cursor plus openConversationId — the exact parameter names and first-page defaults (limit 100, cursor "0") used by helpers.chatMessageSearchCmd. --query maps to keyword, --group maps to openConversationId.
- defensively project each returned message down to {sender, time, text, messageId} (multiple candidate keys per field) and print via rt.Output so it honours --format/--jq/--fields. When the response carries no recognisable message list we fall back to printing the raw payload.
This replaces manually working out the millisecond time window and copying the search incantation. Read-only: it only searches and reshapes, never sends, recalls or marks anything.
dws chat +search-msg --group <openConversationId> --query "changefree" dws chat +search-msg --group <openConversationId> --query "周报" --days 3
var SendToGroup = shortcut.Shortcut{ Service: "chat", Command: "+send-to-group", Product: "chat", Description: "按群名直接给群发消息(自动搜群解析 openConversationId)", Intent: "当你只知道群的名字、想直接往这个群里发一条消息而不想先手动查群 ID 时使用;" + "内部先按群名搜索群聊解析出唯一 openConversationId 再发送,群名匹配到多个群时会列出候选让你区分、绝不自行假定。会真实发出群消息。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群名称(搜群关键词,用群名里连续的核心词)", Required: true}, {Name: "text", Type: shortcut.FlagString, Desc: "消息内容(支持 Markdown)", Required: true}, shortcut.AIMessageTagFlag(), }, Tips: []string{`dws chat +send-to-group --group 项目冲刺 --text "今天 5 点前提交进度"`}, Execute: func(rt *shortcut.RuntimeContext) error { groupName := rt.Str("group") text := rt.Str("text") data, err := rt.CallMCPData("im", "search_groups", map[string]any{ "keyword": groupName, "limit": 10, "cursor": "0", }) if err != nil { return err } groups := extractGroupsForSend(data) switch { case len(groups) == 0: return apperrors.NewValidation(fmt.Sprintf( "没找到名字匹配 %q 的群;换用群名里连续的核心词再试。", groupName)) case len(groups) > 1: return apperrors.NewValidation(fmt.Sprintf( "%q 匹配到 %d 个群:%s。请用更精确的群名,或直接用 dws chat +messages-send --group <openConversationId> 指定群。", groupName, len(groups), strings.Join(sendGroupLabels(groups), "、"))) } content, _ := json.Marshal(map[string]string{"title": text, "text": text}) return rt.CallMCP("send_personal_message", rt.AddAIMessageTag(map[string]any{ "openConversationId": groups[0].id, "msgType": "markdown", "content": string(content), })) }, }
SendToGroup: message a group by its NAME, no openConversationId juggling.
Steps: search groups by name → resolve to a single openConversationId (disambiguate on multiple matches, never guess) → send a markdown message. Replaces `chat search --query <群名>` (copy openConversationId) → `chat +messages-send --group <openConversationId>`.
Note: the group lookup uses `search_groups` (im server, keyword search over group NAMES) — NOT `search_common_groups`, which searches by member nicknames and cannot locate a group by its title.
dws chat +send-to-group --group 项目冲刺 --text "今天 5 点前提交进度"
Service: "doc", Command: "+share-doc", Product: "chat", Description: "按姓名把文档链接私信发给某人(自动解析 userId)", Intent: "当你手上已经有一个文档链接、想直接私信发给某个人而不必先查 userId 时使用;" + "内部先按姓名搜通讯录解析出唯一用户,再用 openDingTalkId 把链接拼成一条 Markdown 消息发出去," + "姓名匹配到多人时会列出候选让你区分。只发链接、不读取或改动文档本身,会真实发出消息。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "to", Type: shortcut.FlagString, Desc: "收件人姓名/花名", Required: true}, {Name: "url", Type: shortcut.FlagString, Desc: "文档链接", Required: true}, {Name: "note", Type: shortcut.FlagString, Desc: "附言(可选)"}, shortcut.AIMessageTagFlag(), }, Tips: []string{`dws doc +share-doc --to 张三 --url https://docs.dingtalk.com/xxx --note "帮忙过一下"`}, Execute: func(rt *shortcut.RuntimeContext) error { url := rt.Str("url") note := rt.Str("note") user, err := resolveUser(rt, rt.Str("to")) if err != nil { return err } if user.openDingTalkID == "" { return apperrors.NewValidation("通讯录结果缺少 openDingTalkId,无法发送文档分享消息;请改用 chat +messages-send --open-dingtalk-id") } text := shareDocBuildText(url, note) title := "文档分享" content, _ := json.Marshal(map[string]string{"title": title, "text": text}) return rt.CallMCP("send_personal_message", rt.AddAIMessageTag(map[string]any{ "receiverOpenDingTalkId": user.openDingTalkID, "msgType": "markdown", "content": string(content), })) }, }
ShareDoc: send a document link to a person by NAME, no ID juggling.
Steps: resolve name → single user (disambiguate on multiple matches) → build a Markdown message that links to the document → send it to the user's openDingTalkId as a single-chat message. This is a pure "share a link" flow: it does not touch any doc tool, it just delivers the URL you already have straight to the recipient's inbox.
dws doc +share-doc --to 张三 --url https://docs.dingtalk.com/xxx --note "帮忙过一下"
var SuggestTime = shortcut.Shortcut{ Service: "calendar", Command: "+suggest-time", Product: "calendar", Description: "按姓名解析多位参与者,推荐大家都有空的可开会时间段(自动解析 userId)", Intent: "当你想为几个人凑一个大家都空闲的开会时间、但只知道他们的姓名而不想逐个手动查 userId 时使用;" + "内部会把 --with 里的姓名逐个搜通讯录解析成唯一 userId(任何一个没匹配到或匹配到多人都会明确报出来,绝不瞎猜)," + "再基于所有参与者的忙闲,在给定时间范围内推荐若干可用时段。只读,不创建任何日程。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "with", Type: shortcut.FlagStringSlice, Desc: "参与者姓名(逗号分隔的 CSV,如 张三,李四)", Required: true}, {Name: "start", Type: shortcut.FlagString, Desc: "时间范围开始(ISO8601,如 2026-03-10T09:00:00+08:00)", Required: true}, {Name: "end", Type: shortcut.FlagString, Desc: "时间范围结束(ISO8601,如 2026-03-10T18:00:00+08:00)", Required: true}, {Name: "duration", Type: shortcut.FlagString, Desc: "会议时长(分钟,可选)", Required: false}, }, Tips: []string{ `dws calendar +suggest-time --with 张三,李四 --start "2026-03-10T09:00:00+08:00" --end "2026-03-10T18:00:00+08:00"`, `dws calendar +suggest-time --with 张三,李四,王五 --start "2026-03-10T09:00:00+08:00" --end "2026-03-10T18:00:00+08:00" --duration 30`, }, Execute: func(rt *shortcut.RuntimeContext) error { names := rt.StrSlice("with") if len(names) == 0 { return apperrors.NewValidation("--with 至少需要一个参与者姓名") } userIDs := make([]string, 0, len(names)) var failures []string for _, raw := range names { name := strings.TrimSpace(raw) if name == "" { continue } user, err := resolveUser(rt, name) if err != nil { failures = append(failures, err.Error()) continue } userIDs = append(userIDs, user.userID) } if len(failures) > 0 { return apperrors.NewValidation( "以下参与者无法解析为唯一 userId:\n- " + strings.Join(failures, "\n- ")) } if len(userIDs) == 0 { return apperrors.NewValidation("--with 至少需要一个有效的参与者姓名") } params := map[string]any{ "start": rt.Str("start"), "end": rt.Str("end"), "attendeeUserIds": userIDs, } if rt.Changed("duration") { params["durationMinutes"] = rt.Str("duration") } data, err := rt.CallMCPData("calendar", "list_suggested_event_times", params) if err != nil { return err } return rt.Output(map[string]any{"suggestions": suggestTimeSlots(data)}) }, }
SuggestTime: recommend meeting slots for several people, resolved BY NAME.
Steps: parse the --with name CSV → resolve each name to a unique userId (fails listing every name that can't be resolved) → ask the calendar service for suggested slots free for everyone in the given range. Replaces looking up each userId by hand and then calling `calendar event suggest --users ...`.
dws calendar +suggest-time --with 张三,李四 \ --start "2026-03-10T09:00:00+08:00" --end "2026-03-10T18:00:00+08:00" --duration 60
var Team = shortcut.Shortcut{ Service: "contact", Command: "+team", Product: "contact", Description: "按姓名列出某人所在部门的成员(自动解析 userId 与 deptId)", Intent: "当你只知道某位同事的姓名、想知道 TA 所在部门里都有哪些成员时使用;" + "内部先按姓名解析出唯一 userId,再取 TA 的组织信息拿到主部门 deptId," + "最后打印该部门的直接成员列表(仅本部门,不递归下级部门)。只读,不做任何修改。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "同事姓名/花名", Required: true}, }, Tips: []string{`dws contact +team --name 张三`}, Execute: func(rt *shortcut.RuntimeContext) error { user, err := resolveUser(rt, rt.Str("name")) if err != nil { return err } data, err := rt.CallMCPData("contact", "get_user_info_by_user_ids", map[string]any{ "user_id_list": []string{user.userID}, }) if err != nil { return err } deptID, ok := shortcutOrgExtractDeptID(data) if !ok { return apperrors.NewValidation(fmt.Sprintf( "没能从 %s(%s) 的组织信息里解析出所在部门 deptId;"+ "TA 可能没有归属部门,或返回结构与预期不符。", user.name, user.userID)) } return rt.CallMCP("get_dept_members_by_deptId", map[string]any{ "deptIds": []string{strconv.FormatInt(deptID, 10)}, }) }, }
Team: list the members of the department a person belongs to, by NAME.
Steps: resolve the person's name → userId → fetch their org detail (get_user_info_by_user_ids) → defensively parse the primary deptId out of orgEmployeeModel.depts → print that department's direct members (get_dept_members_by_deptId). Replaces the manual dance of `contact user search` → copy userId → `contact user get --ids <id>` → copy deptId → `contact dept list-members --depts <deptId>`.
Scope note: get_dept_members_by_deptId returns only the direct members of the resolved department, it does NOT recurse into sub-departments.
dws contact +team --name 张三
var ThisMonthAttendance = shortcut.Shortcut{ Service: "attendance", Command: "+this-month", Product: "attendance", Description: "查我本月的考勤打卡记录(打卡流水,自动解析当前用户)", Intent: "当你想快速看自己本月的打卡流水(几点上下班打卡、打卡地址/定位方式)、又不想先查自己的 userId " + "再手动填写本月的起止时间时使用;内部先取当前登录用户的 userId,再按本地时区算出本月 1 号 00:00 到下月 1 号 00:00 的时间窗," + "最后查询你本月的打卡流水记录。只读操作,不会修改任何考勤数据;本月若还没有任何打卡则返回空结果。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{}, Tips: []string{ `dws attendance +this-month`, }, Execute: func(rt *shortcut.RuntimeContext) error { profile, err := rt.CallMCPData("contact", "get_current_user_profile", nil) if err != nil { return err } userID := myAttendanceCurrentUserID(profile) if userID == "" { return apperrors.NewValidation( "没能解析出当前登录用户的 userId,无法查询你的打卡记录;请确认已登录后重试。") } now := time.Now() start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) end := start.AddDate(0, 1, 0) const layout = "2006-01-02 15:04:05" data, err := rt.CallMCPData("attendance-wukong", "query_check_record", map[string]any{ "QueryCheckRecordRequest": map[string]any{ "userIds": []string{userID}, "checkDateFrom": start.Format(layout), "checkDateTo": end.Format(layout), }, }) if err != nil { return err } return rt.Output(data) }, }
ThisMonthAttendance: show MY punch-in (打卡流水) records for THIS MONTH in one step. It is the month-scoped sibling of +my-attendance (which covers today).
Steps:
- resolve the current logged-in user's userId via the contact server's zero-arg get_current_user_profile (no --name needed — it's always "me");
- compute this month's window [1st 00:00, next-month 1st 00:00) in the local timezone and format both bounds as "yyyy-MM-dd HH:mm:ss" (the format the query_check_record helper feeds the tool);
- call query_check_record on the attendance-wukong server with the exact nested QueryCheckRecordRequest shape used by `dws attendance check record`, then print via rt.Output.
The month window spans at most 31 days, within query_check_record's span cap. Read-only; it never modifies any attendance data.
dws attendance +this-month
var ThreadReplies = shortcut.Shortcut{ Service: "chat", Command: "+thread-replies", Product: "chat", Description: "拉取某条话题消息的全部回复并投影出发言人/文本/时间", Intent: "当你已经拿到某个群里一条「话题消息」的 topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么)," + "而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --topic-id(话题 ID)拉取该话题的回复列表," + "可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。" + "这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群会话 ID(openConversationId,必填)", Required: true}, {Name: "topic-id", Type: shortcut.FlagString, Desc: "话题 ID(由 dws chat message list 返回,必填)", Required: true}, {Name: "time", Type: shortcut.FlagString, Desc: "起始时间,如 \"2025-03-01 00:00:00\"(可选)"}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页拉取的回复条数(可选)"}, }, Tips: []string{ `dws chat +thread-replies --group <openconversationId> --topic-id <topicId>`, `dws chat +thread-replies --group <openconversationId> --topic-id <topicId> --time "2025-03-01 00:00:00" --limit 20`, }, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{ "openconversationId": rt.Str("group"), "topicId": rt.Str("topic-id"), "forward": false, } if rt.Changed("time") && rt.Str("time") != "" { params["startTime"] = rt.Str("time") } if rt.Changed("limit") && rt.Int("limit") > 0 { params["pageSize"] = rt.Int("limit") } data, err := rt.CallMCPData("chat", "list_topic_replies", params) if err != nil { return err } items := threadReplyItems(data) results := make([]map[string]any, 0, len(items)) for _, m := range items { results = append(results, map[string]any{ "sender": threadReplySender(m), "text": threadReplyText(m), "createTime": threadReplyCreateTime(m), }) } return rt.Output(map[string]any{ "replies": results, "count": len(results), }) }, }
ThreadReplies: fetch every reply under one topic ("话题") message and print a clean projected list (speaker / text / time) instead of a raw dump.
Steps:
- call list_topic_replies (chat server) with openconversationId=--group and topicId=--topic-id, optionally startTime=--time and pageSize=--limit — param keys copied verbatim from chat.go's list-topic-replies call site;
- defensively unwrap the reply list (multiple candidate container keys) and project each reply to {sender, text, createTime} tolerating field aliases;
- print via rt.Output as {replies, count} so it honours --format/--jq/--fields.
Read-only: it only reads a topic's replies and reshapes them locally, never posts or mutates anything.
dws chat +thread-replies --group <openconversationId> --topic-id <topicId>
var Today = shortcut.Shortcut{ Service: "calendar", Command: "+today", Product: "calendar", Description: "列出我今天的日程(自动计算今天的起止时间,无需手动填时间范围)", Intent: "当你想快速看看『我今天有哪些日程/会议安排』时使用;" + "内部用本地时区自动把时间范围算成今天 00:00 到次日 00:00,转成毫秒时间戳," + "查询主日历(primary)下今天的全部日程。只读,不会创建或修改任何日程。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{}, Tips: []string{ `dws calendar +today`, }, Execute: func(rt *shortcut.RuntimeContext) error { startOfToday, startOfTomorrow := calendarDayRange(0) toolArgs := map[string]any{ "startTime": startOfToday.UnixMilli(), "endTime": startOfTomorrow.UnixMilli(), "calendarId": "primary", } data, err := rt.CallMCPData("calendar", "list_calendar_events", toolArgs) if err != nil { return err } return rt.Output(map[string]any{"events": calendarProjectEvents(data)}) }, }
Today: list the current user's calendar events for today, with the day boundaries computed automatically so the caller never has to hand-craft ISO/millisecond time ranges.
It resolves "today" from the machine's local clock: startTime = today 00:00, endTime = tomorrow 00:00 (local timezone), both converted to epoch milliseconds — exactly the int64 shape the `list_calendar_events` tool expects at its helper call site (parseISOTimeToMillis -> millis). calendarId defaults to the primary calendar. Replaces the manual `calendar event list --start ... --end ...` where you must format two timezone-aware timestamps by hand.
dws calendar +today
var TodoDone = shortcut.Shortcut{ Service: "todo", Command: "+todo-done", Product: "todo", Description: "按标题关键词把我的某条待办标记完成(自动定位 taskId)", Intent: "当你只记得某条待办的标题关键词、想直接把它标记完成,却不想先翻列表复制 taskId 时使用;" + "内部先拉取你当前组织下作为执行人的待办列表,按标题(subject)包含关键词匹配:没匹配到会提示「没找到匹配待办」," + "匹配到多条会列出候选(标题+taskId)让你写得更精确,唯一命中时才把它标记为已完成。这会真实修改待办完成状态。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "task", Type: shortcut.FlagString, Desc: "待办标题关键词", Required: true}, }, Tips: []string{`dws todo +todo-done --task 周报`}, Execute: func(rt *shortcut.RuntimeContext) error { keyword := strings.TrimSpace(rt.Str("task")) if keyword == "" { return apperrors.NewValidation("请用 --task 提供待办标题关键词") } cards, err := shortcutListAllTodoCards(rt, map[string]any{ "roleTypes": []string{"executor"}, }) if err != nil { return err } matches := shortcutTodoMatch(cards, keyword) switch { case len(matches) == 0: return apperrors.NewValidation(fmt.Sprintf("没找到匹配待办:标题里没有 %q 的待办。", keyword)) case len(matches) > 1: return apperrors.NewValidation(fmt.Sprintf( "%q 匹配到 %d 条待办,请用更精确的关键词,或用 `dws todo task done --task-id` 指定:%s", keyword, len(matches), strings.Join(shortcutTodoLabels(matches), ";"))) } return rt.CallMCP("update_todo_done_status", map[string]any{ "taskId": matches[0].taskID, "isDone": "true", }) }, }
TodoDone: mark one of MY todos complete by matching a keyword in its title.
Steps:
list my todos via get_user_todos_in_current_org (pageNum / pageSize as strings, mirroring helpers.todo list);
scan result.todoCards[] and keep the ones whose subject contains --task; none → "没找到匹配待办", many → list candidates (subject + taskId) so the caller can be more specific, exactly one → take its taskId;
mark it done via update_todo_done_status (taskId + isDone="true", mirroring `dws todo task done`).
dws todo +todo-done --task 周报
var Tomorrow = shortcut.Shortcut{ Service: "calendar", Command: "+tomorrow", Product: "calendar", Description: "列出我明天的日程(自动计算明天的起止时间,无需手动填时间范围)", Intent: "当你想快速看看『我明天有哪些日程/会议安排』、提前准备时使用;" + "内部用本地时区自动把时间范围算成明天 00:00 到后天 00:00,转成毫秒时间戳," + "查询主日历(primary)下明天的全部日程,并投影出标题、开始时间、结束时间、地点、eventId。只读,不会创建或修改任何日程。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{}, Tips: []string{ `dws calendar +tomorrow`, }, Execute: func(rt *shortcut.RuntimeContext) error { startOfTomorrow, startOfDayafter := calendarDayRange(1) toolArgs := map[string]any{ "startTime": startOfTomorrow.UnixMilli(), "endTime": startOfDayafter.UnixMilli(), "calendarId": "primary", } data, err := rt.CallMCPData("calendar", "list_calendar_events", toolArgs) if err != nil { return err } return rt.Output(map[string]any{"events": calendarProjectEvents(data)}) }, }
Tomorrow: list the current user's calendar events for tomorrow, with the day boundaries computed automatically — the mirror of +today shifted one day forward. Handy for "what's on my plate tomorrow" without hand-crafting a time range.
It resolves "tomorrow" from the machine's local clock: startTime = tomorrow 00:00, endTime = day-after 00:00 (local timezone), both converted to epoch milliseconds — the int64 shape list_calendar_events expects. calendarId defaults to the primary calendar. Events are projected to {title,start,end,location,eventId}, identical to +today / +week.
dws calendar +tomorrow
var Transcript = shortcut.Shortcut{ Service: "minutes", Command: "+transcript", Product: "minutes", Description: "取我最新一条妙记(听记)的逐字稿(语音转写原文)", Intent: "当你想直接读回自己最近一次会议听记的逐字稿(完整语音转写原文),又不想先翻列表、复制 taskUuid 再单独查转写时使用;" + "内部先列出你创建的听记(可用 --keyword 缩小范围),自动挑出最新的一条,再拉取它的逐字记录(每条含发言人、文本、时间戳)。" + "可用 --direction 控制排序:0=正序(时间递增,默认),1=倒序。这是只读操作,不会修改任何听记;" + "若你名下没有任何听记则提示「暂无妙记」。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "keyword", Type: shortcut.FlagString, Desc: "按关键字过滤听记(可选)", Required: false}, {Name: "direction", Type: shortcut.FlagString, Desc: "排序方向: 0=正序(默认), 1=倒序(可选)", Required: false}, }, Tips: []string{ `dws minutes +transcript`, `dws minutes +transcript --keyword 周会`, `dws minutes +transcript --direction 1`, }, Execute: func(rt *shortcut.RuntimeContext) error { listArgs := map[string]any{ "belongingConditionId": "created", "maxResults": float64(20), } if kw := rt.Str("keyword"); kw != "" { listArgs["keyword"] = kw } data, err := rt.CallMCPData("minutes", "list_by_keyword_and_time_range", listArgs) if err != nil { return err } taskUUID := latestMinutesTaskUUID(data) if taskUUID == "" { return apperrors.NewValidation("暂无妙记") } direction := rt.Str("direction") if direction == "" { direction = "0" } return rt.CallMCP("get_minutes_transcription", map[string]any{ "taskUuid": taskUUID, "direction": direction, }) }, }
Transcript: fetch the verbatim transcript (逐字稿 / 语音转写原文) of MY most recent minutes (听记) in one step.
Steps:
- list my minutes via list_by_keyword_and_time_range (belongingConditionId = "created"), optionally filtered by --keyword;
- pick the newest entry (largest create time, falling back to the first item) and read its taskUuid — reusing latestMinutesTaskUUID;
- print that minute's verbatim transcript via get_minutes_transcription (taskUuid + direction, mirroring helpers.minutesGetTranscriptionCmd).
If the list is empty it reports "暂无妙记" instead of failing obscurely.
dws minutes +transcript dws minutes +transcript --keyword 周会 dws minutes +transcript --direction 1
var UnreadChats = shortcut.Shortcut{ Service: "chat", Command: "+unread-chats", Product: "chat", Description: "列出我有未读消息的会话(投影会话名/未读数/会话ID)", Intent: "当你想快速看清自己当前有哪些会话还有未读消息、方便逐个处理时使用;" + "内部调用未读会话列表接口,可用 --count 控制返回的会话条数(不传则用服务端默认值)," + "用 --exclude-muted 排除你已设置免打扰的会话;再在本地把每个会话投影成会话名、未读数和会话 ID 三个关键字段。" + "这是纯只读操作,只做列表与本地投影,不会把任何会话标记为已读或未读;若没有未读会话则返回空列表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "count", Type: shortcut.FlagInt, Desc: "返回未读会话条数(可选,不传则使用服务端默认值)", Required: false}, {Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "是否排除已设置免打扰的会话(可选,默认 false)", Required: false}, }, Tips: []string{ `dws chat +unread-chats`, `dws chat +unread-chats --count 20`, `dws chat +unread-chats --exclude-muted`, }, Execute: func(rt *shortcut.RuntimeContext) error { toolArgs := map[string]any{} if count := rt.Int("count"); count > 0 { toolArgs["count"] = count } if rt.Bool("exclude-muted") { toolArgs["excludeMuted"] = true } data, err := rt.CallMCPData("chat", "unread_message_conversation_list", toolArgs) if err != nil { return err } items := unreadChatItems(data) if len(items) == 0 { return rt.Output(data) } results := make([]map[string]any, 0, len(items)) for _, m := range items { row := map[string]any{ "name": unreadChatName(m), "conversationId": unreadChatConversationID(m), } if u := unreadChatUnread(m); u != nil { row["unread"] = u } results = append(results, row) } return rt.Output(map[string]any{"conversations": results}) }, }
UnreadChats: list MY conversations that currently have unread messages in one step.
It calls unread_message_conversation_list on the chat server — the exact tool and parameter names used by helpers.chatMessageListUnreadConversationsCmd. The helper passes count (int) only when > 0 and excludeMuted (bool) only when true, so both are optional; this shortcut mirrors that: --count controls how many conversations to return (0 / unset uses the server default) and --exclude-muted drops chats you have muted.
The returned conversations are then defensively projected down to {name, unread, conversationId} (multiple candidate keys per field). When the response carries no recognisable conversation list we fall back to printing the raw payload via rt.Output so it still honours --format/--jq/--fields.
Read-only: it only lists and reshapes, it never marks anything read/unread.
dws chat +unread-chats dws chat +unread-chats --count 20 dws chat +unread-chats --exclude-muted
var UnreadMail = shortcut.Shortcut{ Service: "mail", Command: "+unread-mail", Product: "mail", Description: "列出未读邮件并投影列表(主题/发件人/时间/messageId)", Intent: "当你想快速看自己邮箱里有哪些未读邮件、并只看一份精简清单(主题、发件人、时间、邮件 messageId)而不想翻完整正文时使用;" + "内部先确定要查的邮箱地址——你可以用 --email 指定,不指定时自动取你绑定的第一个邮箱——" + "再用 KQL 过滤条件 isRead:false 搜索未读邮件," + "最后在本地把每封邮件投影成 {subject, from, date, messageId} 打印出来,可配合 --format/--jq/--fields。" + "这是纯只读操作,只做搜索与本地投影,不会把邮件标记为已读,也不会修改、发送或删除任何邮件;若没有未读邮件则返回空列表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "email", Type: shortcut.FlagString, Desc: "要查询的邮箱地址(可选,默认取你绑定的第一个邮箱)", Required: false}, {Name: "size", Type: shortcut.FlagString, Desc: "返回条数上限(可选,默认 20)", Required: false}, }, Tips: []string{ `dws mail +unread-mail`, `dws mail +unread-mail --email user@company.com`, `dws mail +unread-mail --size 50`, }, Execute: func(rt *shortcut.RuntimeContext) error { email := rt.Str("email") if email == "" { resolved, err := searchMailFirstMailbox(rt) if err != nil { return err } email = resolved } size := rt.Str("size") if size == "" { size = "20" } data, err := rt.CallMCPData("mail", "search_emails", map[string]any{ "email": email, "query": "isRead:false", "size": size, }) if err != nil { return err } messages := searchMailMessages(data) out := make([]map[string]any, 0, len(messages)) for _, m := range messages { out = append(out, map[string]any{ "subject": searchMailFirstString(m, "subject", "title", "topic"), "from": searchMailFrom(m), "date": searchMailFirstAny(m, "date", "sentDate", "receivedDate", "sentTime", "internalDate", "createTime"), "messageId": searchMailFirstString(m, "messageId", "id", "mailId", "emailId", "internetMessageId"), }) } return rt.Output(map[string]any{"messages": out, "email": email}) }, }
UnreadMail: list the current user's unread emails as a compact projection.
Steps:
resolve the mailbox address — use --email when given, otherwise pick the current user's first bound mailbox via list_user_mailboxes (reusing searchMailFirstMailbox from search_mail.go);
search that mailbox via search_emails with the KQL filter isRead:false, matching the isRead field documented in helpers.messageSearch; size is passed as a string, defaulting to "20" like the helper's sizeVal;
in Go, project each returned message to {subject, from, date, messageId} via the shared searchMail* helpers and print the list with rt.Output so it honours --format/--jq/--fields.
Read-only: it only lists and projects, never mutating any mail.
dws mail +unread-mail dws mail +unread-mail --email user@company.com
var Week = shortcut.Shortcut{ Service: "calendar", Command: "+week", Product: "calendar", Description: "列出我本周的日程(自动按周一为周首计算本周起止时间,无需手动填时间范围)", Intent: "当你想快速看看『我本周有哪些日程/会议安排』时使用;" + "内部用本地时区、以周一为一周开始,自动把时间范围算成本周一 00:00 到下周一 00:00,转成毫秒时间戳," + "查询主日历(primary)下本周的全部日程,并投影出标题、开始时间、结束时间、eventId。只读,不会创建或修改任何日程。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{}, Tips: []string{ `dws calendar +week`, }, Execute: func(rt *shortcut.RuntimeContext) error { now := time.Now() startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) offset := (int(startOfDay.Weekday()) + 6) % 7 startOfWeek := startOfDay.AddDate(0, 0, -offset) startOfNextWeek := startOfWeek.AddDate(0, 0, 7) params := map[string]any{ "calendarId": "primary", "startTime": startOfWeek.UnixMilli(), "endTime": startOfNextWeek.UnixMilli(), } data, err := rt.CallMCPData("calendar", "list_calendar_events", params) if err != nil { return err } return rt.Output(map[string]any{"events": calendarProjectEvents(data)}) }, }
Week: list the current user's calendar events for the current week (Monday as the first day of the week), with the week boundaries computed automatically so the caller never has to hand-craft ISO/millisecond time ranges.
It resolves "this week" from the machine's local clock: startTime = this Monday 00:00, endTime = next Monday 00:00 (local timezone), both converted to epoch milliseconds — exactly the int64 shape the `list_calendar_events` tool expects at its helper call site (parseISOTimeToMillis -> millis). calendarId defaults to the primary calendar. The response events are defensively parsed and projected (title / start / end / eventId) via rt.Output so they honour --format/--jq/--fields. Replaces the manual `calendar event list --start ... --end ...` where you must format two timezone-aware timestamps by hand. Read-only.
dws calendar +week
var Whoami = shortcut.Shortcut{ Service: "contact", Command: "+me", Product: "contact", Description: "查看我自己的通讯录资料(姓名/userId/手机/部门/组织,干净投影)", Intent: "当你(或 AI agent)需要知道「我是谁」——我自己的 userId、姓名、所在部门、组织、手机号,用于后续按名解析他人前先确定自己身份、或填充发起人信息时使用;" + "内部调用零参数的 get_current_user_profile(永远是「我」,无需传姓名),再把冗长的原始资料投影成 {name,userId,mobile,dept,org,email} 几个关键字段。" + "这是纯只读操作,不修改任何资料。", Risk: shortcut.RiskRead, Tips: []string{ `dws contact +me`, }, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("contact", "get_current_user_profile", nil) if err != nil { return err } return rt.Output(whoamiProject(data)) }, }
Whoami: print the CURRENT user's own profile as a clean projection — the "who am I" every agent needs before resolving other people. Unlike the 1:1 +get-self (which dumps the verbose raw {result:[{orgEmployeeModel:{…}}]}), this projects to {name, userId, mobile, dept, org, email}.
It calls the zero-arg get_current_user_profile (always "me", no --name), mirroring the resolution +my-attendance already relies on.
dws contact +me
var WikiNewDoc = shortcut.Shortcut{ Service: "wiki", Command: "+wiki-new-doc", Product: "wiki", Description: "在指定名称的知识库下新建一个文档节点(自动按空间名解析 workspaceId)", Intent: "当你只知道知识库(知识空间)的名字、想直接在它下面新建一篇文档,却不想先搜索空间、复制 workspaceId 再建节点时使用;" + "内部先按空间名搜索知识库,若唯一命中则拿到它的 workspaceId,再在该库根目录下创建一个在线文档节点。" + "如果这个名字没有匹配到任何知识库,或匹配到多个,会报错让你用更精确的名字,绝不乱猜。" + "这会真实创建一个新的文档节点。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "space", Type: shortcut.FlagString, Desc: "知识库(知识空间)名称", Required: true}, {Name: "title", Type: shortcut.FlagString, Desc: "新建文档的标题", Required: true}, }, Tips: []string{ `dws wiki +wiki-new-doc --space "产品文档库" --title "需求评审纪要"`, }, Execute: func(rt *shortcut.RuntimeContext) error { spaceName := strings.TrimSpace(rt.Str("space")) title := strings.TrimSpace(rt.Str("title")) if spaceName == "" { return apperrors.NewValidation("--space 不能为空") } if title == "" { return apperrors.NewValidation("--title 不能为空") } data, err := rt.CallMCPData("wiki", "search_wikiSpaces", map[string]any{ "keyword": spaceName, }) if err != nil { return err } workspaceID, err := wikiNewDocResolveSpaceID(data, spaceName) if err != nil { return err } if rt.DryRun() { return rt.Output(map[string]any{ "dryRun": true, "space": spaceName, "title": title, "wouldCreateIn": workspaceID, }) } created, err := rt.CallMCPWriteData("doc", "create_file", map[string]any{ "workspaceId": workspaceID, "name": title, "type": "adoc", }) if err != nil { return err } return rt.Output(map[string]any{ "created": true, "space": spaceName, "title": title, "result": created, }) }, }
WikiNewDoc: create a new document node inside a knowledge space identified BY NAME, in one command.
Steps: search knowledge spaces by the given name (search_wikiSpaces) → resolve exactly one space to its workspaceId (0 or >1 matches → a clear disambiguation error, never a guess) → create an online document node under that space's root (create_file on the doc MCP server, mirroring `dws wiki node create`). Replaces the manual dance of `dws wiki space search --query <name>` (copy the workspaceId) → `dws wiki node create --workspace <id> --name <title>`.
dws wiki +wiki-new-doc --space "产品文档库" --title "需求评审纪要"
Functions ¶
This section is empty.
Types ¶
This section is empty.
Source Files
¶
- action_items.go
- approve.go
- assign.go
- assign_multi.go
- at_me.go
- book.go
- broadcast.go
- by_mobile.go
- calendar_shared.go
- cancel_event.go
- chat_messages.go
- conflicts.go
- created_todos.go
- dept_members.go
- dm.go
- doc_append.go
- done_approvals.go
- due_today.go
- find_doc.go
- find_file.go
- find_mail_user.go
- find_record.go
- find_room.go
- free_slots.go
- freebusy.go
- group_members.go
- invite.go
- latest_minutes.go
- list_tables.go
- lookup.go
- minutes_detail.go
- minutes_search.go
- my_attendance.go
- my_free.go
- my_groups.go
- my_initiated.go
- next_event.go
- org.go
- overdue.go
- pending_approvals.go
- recent_mail.go
- record_share_links.go
- related_tasks.go
- remind.go
- replace_batch.go
- report_latest.go
- reschedule.go
- resolve.go
- resolve_base.go
- resolve_dept.go
- resolve_space.go
- resolve_table.go
- respond_event.go
- search_mail.go
- search_msg.go
- send_to_group.go
- share_doc.go
- suggest_time.go
- team.go
- this_month_attendance.go
- thread_replies.go
- today.go
- todo_done.go
- todo_shared.go
- tomorrow.go
- transcript.go
- unread_chats.go
- unread_mail.go
- week.go
- whoami.go
- wiki_new_doc.go