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 AccessChange = shortcut.Shortcut{ Service: "doc", Command: "+access-change", Product: "doc", Description: "预检已有协作者后变更文档角色", Intent: "当用户要修改文档上已有协作者的权限角色时使用;先按姓名解析并读取当前权限,目标不是现有协作者时停止,不把 update 当 add。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"}, Contract: docSmartContract("+access-change", "预检已有协作者后变更文档角色", "当用户要修改文档上已有协作者的权限角色时使用;先按姓名解析并读取当前权限,目标不是现有协作者时停止,不把 update 当 add。", []string{`dws doc +access-change --node <DOC_ID> --to 张三 --role EDITOR`}, false), Flags: permissionFlags(true), Tips: []string{`dws doc +access-change --node <DOC_ID> --to 张三 --role EDITOR`}, Execute: func(rt *shortcut.RuntimeContext) error { users, err := resolveDocUsers(rt, false) if err != nil { return err } current, err := rt.CallMCPData("doc", "list_permission", map[string]any{"nodeId": rt.Str("node")}) if err != nil { return err } missing := usersMissingPermission(current, users) if len(missing) > 0 { return apperrors.NewValidation(fmt.Sprintf("以下用户不是当前直接协作者,不能 change;请改用 access-grant: %v", missing)) } params := permissionParams(rt, users) if rt.DryRun() { return rt.Output(docAccessEnvelope("doc.access_change", map[string]any{"executed": false, "preflight": "existing_collaborators", "params": params})) } result, err := rt.CallMCPWriteData("doc", "update_permission", params) if err != nil { return err } return rt.Output(docAccessEnvelope("doc.access_change", result)) }, }
var AccessGrant = shortcut.Shortcut{ Service: "doc", Command: "+access-grant", Product: "doc", Description: "按姓名解析后批量授予文档权限", Intent: "当用户要给一个或多位同事授予单篇文档 READER/DOWNLOADER/EDITOR/MANAGER 权限时使用;所有姓名唯一解析成功后才执行一次批量授权。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"}, Contract: docSmartContract("+access-grant", "按姓名解析后批量授予文档权限", "当用户要给一个或多位同事授予单篇文档 READER/DOWNLOADER/EDITOR/MANAGER 权限时使用;所有姓名唯一解析成功后才执行一次批量授权。", []string{`dws doc +access-grant --node <DOC_ID> --to 张三,李四 --role READER`}, false), Flags: permissionFlags(true), Tips: []string{`dws doc +access-grant --node <DOC_ID> --to 张三,李四 --role READER`}, Execute: func(rt *shortcut.RuntimeContext) error { users, err := resolveDocUsers(rt, false) if err != nil { return err } params := permissionParams(rt, users) if rt.DryRun() { return rt.Output(docAccessEnvelope("doc.access_grant", map[string]any{"executed": false, "resolved": resolvedUserLedger(users), "params": params})) } result, err := rt.CallMCPWriteData("doc", "add_permission", params) if err != nil { return err } return rt.Output(docAccessEnvelope("doc.access_grant", map[string]any{"resolved": resolvedUserLedger(users), "result": result})) }, }
var AccessRevoke = shortcut.Shortcut{ Service: "doc", Command: "+access-revoke", Product: "doc", Description: "预检并移除指定协作者的文档权限", Intent: "当用户明确要撤销一位或多位现有协作者对单篇文档的直接权限时使用;先解析姓名并读取权限预检,再执行高风险移除。", Risk: shortcut.RiskHighWrite, Safety: contract.SafetySpec{Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown"}, Contract: docSmartContract("+access-revoke", "预检并移除指定协作者的文档权限", "当用户明确要撤销一位或多位现有协作者对单篇文档的直接权限时使用;先解析姓名并读取权限预检,再执行高风险移除。", []string{`dws doc +access-revoke --node <DOC_ID> --to 张三`}, false), Flags: permissionFlags(false), Tips: []string{`dws doc +access-revoke --node <DOC_ID> --to 张三`}, Execute: func(rt *shortcut.RuntimeContext) error { users, err := resolveDocUsers(rt, false) if err != nil { return err } current, err := rt.CallMCPData("doc", "list_permission", map[string]any{"nodeId": rt.Str("node")}) if err != nil { return err } missing := usersMissingPermission(current, users) if len(missing) > 0 { return apperrors.NewValidation(fmt.Sprintf("以下用户没有可移除的直接权限: %v", missing)) } params := map[string]any{"nodeId": rt.Str("node"), "userIds": docUserIDs(users)} if rt.Str("workspace") != "" { params["workspaceId"] = rt.Str("workspace") } if rt.DryRun() { return rt.Output(docAccessEnvelope("doc.access_revoke", map[string]any{"executed": false, "preflight": "existing_collaborators", "params": params})) } result, err := rt.CallMCPWriteData("doc", "remove_permission", params) if err != nil { return err } return rt.Output(docAccessEnvelope("doc.access_revoke", result)) }, }
var ActionItems = shortcut.Shortcut{ Service: "minutes", Command: "+action-items", Product: "minutes", Description: "读取指定或我最新一条听记中已抽取的行动项", Intent: "当你要读取已知 taskUuid(--id)的听记行动项,或不传 --id 自动选择自己最新听记时使用;" + "只接受服务端显式 actions/dingtalkTodoList 数组,合法空数组表示没有抽取到行动项,缺字段或错误响应不会被当成空成功。" + "这是只读的 Minutes 产物读取,不会创建或修改钉钉 Todo。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: minutesSmartContract( "+action-items", "读取最新听记中已抽取的行动项", "要读取指定听记(--id)或默认最新听记中由听记服务抽取的 actions/dingtalkTodoList 时使用;该命令只读,不会创建钉钉待办。", []string{"要创建或修改真正的钉钉待办时使用 Todo 产品命令"}, []string{"dws minutes +action-items --id <taskUuid>", "dws minutes +action-items"}, nil, ), Flags: []shortcut.Flag{ {Name: "id", Type: shortcut.FlagString, Desc: "听记 taskUuid;不传时选择我最新的一条"}, }, Tips: []string{ `dws minutes +action-items --id <taskUuid>`, `dws minutes +action-items`, }, Execute: func(rt *shortcut.RuntimeContext) error { taskUUID := rt.Str("id") if taskUUID == "" { 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, err = latestMinutesTaskUUID(data) if err != nil { return err } if taskUUID == "" { return apperrors.NewValidation("暂无妙记") } } todosData, err := rt.CallMCPData("minutes", "list_minutes_todos", map[string]any{ "taskUuid": taskUUID, }) if err != nil { return err } if err := minutesdata.ValidateArtifact("todos", taskUUID, todosData); err != nil { return err } return rt.Output(todosData["result"]) }, }
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 by an explicit comparable timestamp 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 Assign = shortcut.Shortcut{ OutputRollout: output.RolloutUnifiedActive, Service: "todo", Command: "+assign", Product: "todo", Description: "按姓名给某人创建并指派一条待办(自动解析 userId)", Intent: "当你想把一件事指派给某位同事、但只知道对方姓名不想先查 userId 时使用;" + "内部先按姓名解析出唯一 userId,再创建待办并把 TA 设为执行人。会真实创建待办。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "todo", Name: "shortcut_assign", CanonicalPath: "todo.shortcut_assign", CLIPath: "todo +assign", PrimaryCLIPath: "todo +assign", }, Description: "按姓名给某人创建并指派一条待办(自动解析 userId)", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"已验证的单人指派待办","properties":{"taskId":{"type":"string","description":"新待办稳定 taskId"},"subject":{"type":"string","description":"待办标题"},"executorId":{"type":"string","description":"解析出的执行人 userId"},"verified":{"type":"boolean","description":"是否完成详情读回核验"}},"required":["taskId","subject","executorId","verified"],"additionalProperties":false}`)}, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名给某人创建并指派一条待办(自动解析 userId)", UseWhen: []string{"当你想把一件事指派给某位同事、但只知道对方姓名不想先查 userId 时使用;内部先按姓名解析出唯一 userId,再创建待办并把 TA 设为执行人。会真实创建待办。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws todo +assign --to 张三 --task \"整理周报\""}, }, }, 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 } params := map[string]any{ "PersonalTodoCreateVO": vo, } if rt.DryRun() { return rt.Output(map[string]any{"dryRun": true, "executed": false, "subject": rt.Str("task")}) } data, err := rt.CallMCPWriteDataStrict("todo", "create_personal_todo", params) if err != nil { return err } taskID, _, err := todoshortcut.VerifyCreatedTodo(rt, data, "todo/create_personal_todo", rt.Str("task")) if err != nil { return err } return rt.Output(map[string]any{"taskId": taskID, "subject": rt.Str("task"), "executorId": user.userID, "verified": true}) }, }
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{ OutputRollout: output.RolloutUnifiedActive, Service: "todo", Command: "+assign-multi", Product: "todo", Description: "把一条待办按姓名一次性指派给多个人(自动把每个姓名解析成 userId)", Intent: "当你想把同一条待办同时指派给好几个同事、但手上只有他们的姓名而不是 userId 时使用;" + "内部会把 --to 里的每个姓名逐个解析成唯一 userId,只要有任何一个姓名查不到或者重名有歧义," + "就把这些问题一次性汇总报错、并且完全不创建待办(不会建出只指派了一半人的残缺待办)。" + "全部姓名都解析成功后,才用这些 userId 一次性创建这条待办并指派给所有人。会真实创建一条新的待办。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "todo", Name: "shortcut_assign_multi", CanonicalPath: "todo.shortcut_assign_multi", CLIPath: "todo +assign-multi", PrimaryCLIPath: "todo +assign-multi", }, Description: "把一条待办按姓名一次性指派给多个人(自动把每个姓名解析成 userId)", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"已验证的多人指派待办","properties":{"taskId":{"type":"string","description":"新待办稳定 taskId"},"subject":{"type":"string","description":"待办标题"},"executors":{"type":"array","description":"已解析执行人摘要","items":{"type":"string"}},"count":{"type":"integer","description":"执行人数"},"verified":{"type":"boolean","description":"是否完成详情读回核验"}},"required":["taskId","subject","executors","count","verified"],"additionalProperties":false}`)}, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "把一条待办按姓名一次性指派给多个人(自动把每个姓名解析成 userId)", UseWhen: []string{"当你想把同一条待办同时指派给好几个同事、但手上只有他们的姓名而不是 userId 时使用;内部会把 --to 里的每个姓名逐个解析成唯一 userId,只要有任何一个姓名查不到或者重名有歧义,就把这些问题一次性汇总报错、并且完全不创建待办(不会建出只指派了一半人的残缺待办)。全部姓名都解析成功后,才用这些 userId 一次性创建这条待办并指派给所有人。会真实创建一条新的待办。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws todo +assign-multi --to \"张三,李四\" --task \"周五前提交排期\""}, }, }, 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,已中止") } params := map[string]any{ "PersonalTodoCreateVO": map[string]any{ "subject": task, "executorIds": executorIDs, }, } if rt.DryRun() { return rt.Output(map[string]any{"dryRun": true, "executed": false, "subject": task, "count": len(executorIDs)}) } data, err := rt.CallMCPWriteDataStrict("todo", "create_personal_todo", params) if err != nil { return err } taskID, _, err := todoshortcut.VerifyCreatedTodo(rt, data, "todo/create_personal_todo", task) if err != nil { return err } return rt.Output(map[string]any{ "taskId": taskID, "subject": task, "executors": resolved, "count": len(executorIDs), "verified": true, }) }, }
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 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息," + "再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。" + "默认只读且不会发送、撤回或标记任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_at_me", CanonicalPath: "chat.shortcut_at_me", CLIPath: "chat +at-me", PrimaryCLIPath: "chat +at-me", }, Description: "查最近 @我 的消息(自动算时间窗,投影发送人/时间/内容/会话)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "查最近 @我 的消息(自动算时间窗,投影发送人/时间/内容/会话)", UseWhen: []string{"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。默认只读且不会发送、撤回或标记任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws chat +at-me", "dws chat +at-me --days 3", }, }, }, Flags: append(append([]shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "仅查看指定群;可传 openConversationId 或群名"}, {Name: "chat-query", Type: shortcut.FlagString, Desc: "--group 的旧版自然名称入口", Hidden: true}, {Name: "group-query", Type: shortcut.FlagString, Desc: "--chat-query 的兼容别名", Hidden: true}, {Name: "days", Type: shortcut.FlagInt, Desc: "回溯天数(默认 7);--days 必须在 1-3650 之间", Default: "7", Required: false}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页返回数量(默认 50);--limit 必须大于 0", Default: "50"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传上次的 nextCursor", Default: "0"}, {Name: "page-all", Type: shortcut.FlagBool, Desc: "沿 nextCursor 自动读取全部 @我 消息;--page-limit 仅与 --page-all 一起使用且范围 1-500;--max-items/--page-delay 仅与 --page-all 一起使用;值必须大于等于 0"}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, {Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出消息 reaction(默认输出)"}, }, shortcut.AutoPageControlFlags()...), chatshortcut.MessageResourceDownloadFlags()...), Constraints: append(append([]shortcut.Constraint{ {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"group", "chat-query", "group-query"}}, {Kind: shortcut.ConstraintCustom, Flags: []string{"days"}, Description: "--days 必须在 1-3650 之间"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"limit"}, Description: "--limit 必须大于 0"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, }, shortcut.AutoPageControlConstraints()...), chatshortcut.MessageResourceDownloadConstraints()...), Tips: []string{ `dws chat +at-me`, `dws chat +at-me --days 3`, `dws chat +at-me --group "项目群"`, `dws chat +at-me --days 30 --page-all --page-limit 50`, }, Validate: validateAtMe, Execute: executeAtMe, }
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. The default path only searches and reshapes; --download-resources additionally writes resource files locally.
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, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_book", CanonicalPath: "calendar.shortcut_book", CLIPath: "calendar +book", PrimaryCLIPath: "calendar +book", }, Description: "创建日程,并可按姓名邀请参会人(自动解析 userId,失败自动回滚删除日程)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "创建日程,并可按姓名邀请参会人(自动解析 userId,失败自动回滚删除日程)", UseWhen: []string{"当你想快速排一个会/日程、并顺手把几位同事按姓名拉进来时使用;内部先建日程拿到 eventId,再把每个姓名解析成唯一 userId 批量加为参会人。如果加参会人失败,会自动删除刚建好的日程回滚,避免留下一个没人的空日程。会真实创建日程并发出参会邀请。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws calendar +book --title \"周会\" --start \"2026-03-10T14:00:00+08:00\" --end \"2026-03-10T15:00:00+08:00\"", "dws calendar +book --title \"Q1 复盘会\" --start \"2026-03-10T14:00:00+08:00\" --end \"2026-03-10T15:00:00+08:00\" --with 张三,李四", }, }, }, Flags: []shortcut.Flag{ {Name: "title", Type: shortcut.FlagString, Desc: "日程标题", Required: true}, {Name: "start", Type: shortcut.FlagString, Desc: "开始时间(ISO8601,如 2026-03-10T14:00:00+08:00)", Required: true}, {Name: "end", Type: shortcut.FlagString, Desc: "结束时间(ISO8601,如 2026-03-10T15:00:00+08:00)", Required: true}, {Name: "with", Type: shortcut.FlagString, Desc: "参会人姓名,逗号分隔(可选)"}, }, Tips: []string{ `dws calendar +book --title "周会" --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00"`, `dws calendar +book --title "Q1 复盘会" --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" --with 张三,李四`, }, Execute: func(rt *shortcut.RuntimeContext) error { start := rt.Str("start") end := rt.Str("end") if err := calendarSmartValidateRange(start, end); err != nil { return err } createArgs := map[string]any{ "summary": rt.Str("title"), "startDateTime": start, "endDateTime": end, } // 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 var userNames []string withPeople := rt.Changed("with") && strings.TrimSpace(rt.Str("with")) != "" if withPeople { 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) userNames = append(userNames, user.name) } if len(userIDs) == 0 { return apperrors.NewValidation("--with 需要至少一个有效的参会人姓名") } } if rt.DryRun() { return rt.Output(map[string]any{ "success": true, "dryRun": true, "executed": false, "wouldCreate": createArgs, "inviteeCount": len(userIDs), }) } created, err := rt.CallMCPWriteDataStrict("calendar", "create_calendar_event", createArgs) if err != nil { return err } if err := calendarSmartWriteReceipt(created, "calendar/create_calendar_event"); err != nil { return err } eventID := calendarSmartEventID(created) if eventID == "" { return calendarSmartError("calendar/create_calendar_event", "missing_event_id", "创建回执缺少稳定日程 id,远端效果未知") } if len(userIDs) > 0 { added, addErr := rt.CallMCPWriteDataStrict("calendar", "add_calendar_participant", map[string]any{ "eventId": eventID, "attendeesToAdd": userIDs, }) if addErr == nil { addErr = calendarSmartWriteReceipt(added, "calendar/add_calendar_participant") } if addErr != nil { if rollbackErr := calendarSmartDeleteAndVerify(rt, eventID); rollbackErr != nil { return apperrors.NewValidation(fmt.Sprintf( "添加参会人失败:%v;回滚删除或删除后验证失败:%v,请人工核查日程状态", addErr, rollbackErr)) } return apperrors.NewValidation(fmt.Sprintf("添加参会人失败:%v;新建日程已回滚并验证不存在", addErr)) } } readback, err := rt.CallMCPData("calendar", "get_calendar_detail", map[string]any{"eventId": eventID}) if err != nil { return err } event, err := calendarSmartRequireEvent(readback, "calendar/get_calendar_detail", eventID) if err != nil { return err } if err := calendarSmartVerifyCreatedEvent(event, eventID, rt.Str("title"), start, end); err != nil { return err } if len(userIDs) > 0 { participants, err := rt.CallMCPData("calendar", "get_calendar_participants", map[string]any{"eventId": eventID}) if err != nil { return err } present, err := calendarSmartAttendees(participants) if err != nil { return err } currentUserID, err := calendarSmartCurrentUserID(rt, present) if err != nil { return err } if err := calendarSmartVerifyAttendees(present, userIDs, userNames, currentUserID); err != nil { return err } } return rt.Output(map[string]any{ "success": true, "eventId": eventID, "verified": true, "event": event, }) }, }
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, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_broadcast", CanonicalPath: "chat.shortcut_broadcast", CLIPath: "chat +broadcast", PrimaryCLIPath: "chat +broadcast", }, Description: "按姓名逐一给多个人群发同一条单聊消息(自动解析 userId、逐个发送)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名逐一给多个人群发同一条单聊消息(自动解析 userId、逐个发送)", UseWhen: []string{"当你想把同一条通知一次性单聊发给多位同事、但只知道他们的姓名不想逐个查 userId 时使用;内部把姓名列表逐个解析成唯一用户后,用 openDingTalkId 对每个人单独发一条单聊消息,并汇总成功/失败人数。某个姓名匹配不到人或匹配到多人时,会跳过该人并在结尾报出,不影响其他人收到消息。会真实发出多条消息。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +broadcast --to \"张三,李四,王五\" --content \"今晚 8 点上线,请留意\""}, }, Parameters: []contract.ParamDecl{renamedRequiredParam("content", "text")}, }, Flags: []shortcut.Flag{ {Name: "to", Type: shortcut.FlagStringSlice, Desc: "收件人姓名/花名,逗号分隔的多个人", Required: true}, {Name: "content", Type: shortcut.FlagString, Desc: "消息内容(支持 Markdown),所有人收到同一条", Required: true, Aliases: []string{"text"}}, shortcut.AIMessageTagFlag(), }, Tips: []string{`dws chat +broadcast --to "张三,李四,王五" --content "今晚 8 点上线,请留意"`}, Execute: func(rt *shortcut.RuntimeContext) error { text := rt.StrFirst("text", "content") 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 plans []map[string]any ) for _, raw := range names { name := strings.TrimSpace(raw) if name == "" { continue } resolved, err := targetresolver.ResolveEnterpriseUser(rt, name, targetresolver.IdentityAny) if err != nil { failed = append(failed, fmt.Sprintf("%s(%s)", name, err.Error())) continue } user := resolved.Selected targetArgs := map[string]any{} if user.OpenDingTalkID != "" { targetArgs["receiverOpenDingTalkId"] = user.OpenDingTalkID } else { targetArgs["receiverUserId"] = user.UserID } recipient := user.Name if recipient == "" { recipient = name } messageArgs := rt.AddAIMessageTag(map[string]any{ "msgType": "markdown", "content": string(content), }) for key, value := range targetArgs { messageArgs[key] = value } if rt.DryRun() { plans = append(plans, map[string]any{ "recipient": recipient, "tool": "send_personal_message", "arguments": messageArgs, }) sent = append(sent, recipient) continue } if _, err := rt.CallMCPWriteData("chat", "send_personal_message", messageArgs); err != nil { failed = append(failed, fmt.Sprintf("%s(发送失败:%s)", name, err.Error())) continue } sent = append(sent, recipient) } 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 } if rt.DryRun() { result["dry_run"] = true result["executed"] = false result["preview_kind"] = "plan" result["tool"] = "send_personal_message" result["actionCount"] = len(plans) result["actions"] = plans } 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 "张三,李四,王五" --content "今晚 8 点上线,请留意群公告"
var ByMobile = shortcut.Shortcut{ Service: "contact", Command: "+by-mobile", Product: "contact", Description: "按手机号查询某人的完整资料(自动解析 userId 后取详情)", Intent: "当你只知道对方手机号、想一步拿到其完整资料(部门、职位、联系方式、是否管理员等)而不想先按手机号搜出 userId 再单独查详情时使用;" + "内部先用手机号在通讯录里查出对应的 userId,若没有人绑定该手机号会明确报错,再用该 userId 取完整详情。这是纯只读操作,不会修改任何数据。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "shortcut_by_mobile", CanonicalPath: "contact.shortcut_by_mobile", CLIPath: "contact +by-mobile", PrimaryCLIPath: "contact +by-mobile", }, Description: "按手机号查询某人的完整资料(自动解析 userId 后取详情)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按手机号查询某人的完整资料(自动解析 userId 后取详情)", UseWhen: []string{"当你只知道对方手机号、想一步拿到其完整资料(部门、职位、联系方式、是否管理员等)而不想先按手机号搜出 userId 再单独查详情时使用;内部先用手机号在通讯录里查出对应的 userId,若没有人绑定该手机号会明确报错,再用该 userId 取完整详情。这是纯只读操作,不会修改任何数据。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws contact +by-mobile --mobile 13800138000"}, }, }, Flags: []shortcut.Flag{ {Name: "mobile", Type: shortcut.FlagString, Desc: "手机号;--mobile 必须是至少 6 位数字的手机号,可包含国家码、空格、连字符或括号", Required: true}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"mobile"}, Description: "--mobile 必须是至少 6 位数字的手机号,可包含国家码、空格、连字符或括号"}}, Validate: func(rt *shortcut.RuntimeContext) error { return validateContactSmartMobile(rt, "contact/by-mobile", "mobile") }, 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") _, profile, err := strictResolveContactUserByMobile(rt, mobile) if err != nil { return err } return rt.Output(map[string]any{"profile": profile}) }, }
ByMobile: find a person by phone number and return their full profile in one step.
Steps: resolve the number through the dedicated exact-mobile interface → fetch the same stable userId's full detail. The detail endpoint does not expose mobile under every permission profile, so identity is bound to the dedicated lookup result rather than inferred from keyword search.
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, Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_cancel_event", CanonicalPath: "calendar.shortcut_cancel_event", CLIPath: "calendar +cancel-event", PrimaryCLIPath: "calendar +cancel-event", }, Description: "取消(删除)一个已有日程(删除前先确认它真实存在)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "取消(删除)一个已有日程(删除前先确认它真实存在)", UseWhen: []string{"当你想取消/删除一个已经存在的日程时使用;内部先用 eventId 拉一次日程详情确认它真实存在,再执行删除并验证它已不存在,避免因 eventId 写错而误删别的日程。如果 eventId 查不到会直接报错,不会盲目删除。这是高危写操作,会真实删除该日程,框架会二次确认。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws calendar +cancel-event --event EVENT_ID"}, }, }, 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 } if _, err := calendarSmartRequireEvent(detail, "calendar/get_calendar_detail", eventID); err != nil { return err } if rt.DryRun() { return rt.Output(map[string]any{ "success": true, "dryRun": true, "executed": false, "eventId": eventID, }) } if err := calendarSmartDeleteAndVerify(rt, eventID); err != nil { return err } return rt.Output(map[string]any{ "success": true, "eventId": eventID, "deleted": true, "verified": true, }) }, }
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). Then delete it via delete_calendar_event and verify the event is absent. 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 ChatMembersList = shortcut.Shortcut{ Service: "chat", Command: "+chat-members-list", Aliases: []string{"+chat-group-members"}, Product: "chat", Description: "列出群成员并把用户与机器人分桶(支持群名语义解析)", Intent: "当你要完整查看一个群的参与者,并需要区分真人用户和机器人时使用;" + "--group 可传群名或 openConversationId,也可用 --conversation-id 显式传稳定 ID、用 --chat-query 显式按群名解析。" + "默认同时返回 users/bots 两个桶,也可用 --member-types 只取 user 或 bot;" + "用户桶自动翻页并按稳定 ID 去重,结果用 buckets、complete、hasMore、nextCursor 和 failures 证明完整性;--page-limit 保证有界。机器人桶按下层全量列表投影。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_chat_members_list", CanonicalPath: "chat.shortcut_chat_members_list", CLIPath: "chat +chat-members-list", PrimaryCLIPath: "chat +chat-members-list", Aliases: []string{"chat +chat-group-members"}, }, Description: "列出群成员并把用户与机器人分桶(支持群名语义解析)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed composite member adapter: the executable CLI safely resolves a group, paginates and deduplicates user members, lists bots, and publishes per-bucket completeness and failures.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出群成员并把用户与机器人分桶(支持群名语义解析)", UseWhen: []string{"当你要完整查看一个群的参与者,并需要区分真人用户和机器人时使用;" + "--group 可传群名或 openConversationId,也可用 --conversation-id 显式传稳定 ID、用 --chat-query 显式按群名解析。" + "默认同时返回 users/bots 两个桶,也可用 --member-types 只取 user 或 bot;" + "用户桶自动翻页并按稳定 ID 去重,结果用 buckets、complete、hasMore、nextCursor 和 failures 证明完整性;--page-limit 保证有界。机器人桶按下层全量列表投影。"}, AvoidWhen: []string{"只需要用户成员且已有群名时可使用 +group-members;需要原始单页响应时使用底层原子命令"}, Examples: []string{ "dws chat +chat-members-list --group \"项目冲刺\"", "dws chat +chat-members-list --conversation-id <openConversationId> --member-types user,bot", }, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群名称或 openConversationId"}, {Name: "conversation-id", Type: shortcut.FlagString, Desc: "显式群 openConversationId"}, {Name: "chat-query", Type: shortcut.FlagString, Desc: "按群名解析唯一 openConversationId"}, {Name: "chat", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true}, {Name: "open-conversation-id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true}, {Name: "member-types", Type: shortcut.FlagStringSlice, Desc: "成员类型;--member-types 仅接受 user/bot;不传则同时返回"}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "用户成员桶最大页数;--page-limit 必须在 1-500 之间"}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id", "chat-query", "chat", "open-conversation-id"}}, {Kind: shortcut.ConstraintCustom, Flags: []string{"member-types"}, Description: "--member-types 仅接受 user/bot"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-limit"}, Description: "--page-limit 必须在 1-500 之间"}, }, Tips: []string{ `dws chat +chat-members-list --group "项目冲刺"`, `dws chat +chat-members-list --conversation-id <openConversationId> --member-types user,bot`, }, Validate: validateGroupMembersPageLimit, Execute: func(rt *shortcut.RuntimeContext) error { groupID := strings.TrimSpace(rt.StrFirst("conversation-id", "chat", "open-conversation-id")) if groupID == "" { resolved, err := targetresolver.ResolveChatTarget(rt, rt.Str("group"), rt.Str("chat-query")) if err != nil { return err } groupID = resolved.Selected.OpenConversationID } wantUsers, wantBots, err := resolveMemberTypes(rt.StrSlice("member-types")) if err != nil { return err } payload := map[string]any{ "contractVersion": groupMembersContractVersion, "conversationId": groupID, "users": []map[string]any{}, "bots": []map[string]any{}, } buckets := map[string]any{} var userErr, botErr error var userResult groupUserMembersResult if wantUsers { userResult, userErr = collectGroupUserMembers(rt, groupID, rt.Int("page-limit")) if userErr == nil { payload["users"] = userResult.members buckets["users"] = userResult.bucketPayload() } } if wantBots { var data map[string]any data, botErr = rt.CallMCPData("bot", "list_group_bots", map[string]any{ "openConversationId": groupID, }) if botErr == nil { bots := groupBotProject(data) payload["bots"] = bots buckets["bots"] = map[string]any{ "complete": true, "count": len(bots), } } } if wantUsers && userErr != nil && !wantBots { return userErr } if wantBots && botErr != nil && !wantUsers { return botErr } if userErr != nil && botErr != nil { return fmt.Errorf("读取用户成员失败: %v;读取机器人失败: %v", userErr, botErr) } failures := make([]map[string]any, 0, 1+len(userResult.failures)) failures = append(failures, userResult.failures...) if userErr != nil { failures = append(failures, map[string]any{"bucket": "users", "stage": "read", "error": userErr.Error()}) } if botErr != nil { failures = append(failures, map[string]any{"bucket": "bots", "stage": "read", "error": botErr.Error()}) } if len(failures) > 0 { payload["errors"] = failures payload["partial"] = true } else { payload["partial"] = false } users, _ := payload["users"].([]map[string]any) bots, _ := payload["bots"].([]map[string]any) payload["counts"] = map[string]any{ "users": len(users), "bots": len(bots), "total": len(users) + len(bots), } payload["buckets"] = buckets payload["failedCount"] = len(failures) payload["failures"] = failures payload["complete"] = len(failures) == 0 && (!wantUsers || userResult.complete) payload["hasMore"] = wantUsers && userResult.hasMore if wantUsers && userResult.nextCursor != "" { payload["nextCursor"] = userResult.nextCursor } return rt.Output(payload) }, }
ChatMembersList aligns the recent lark-cli IM member-list experience with DingTalk's two lower tools: user members come from get_group_members and bots come from list_group_bots. Callers may provide a stable conversation ID or a human group name; name resolution remains ambiguity-safe.
var ChatMessages = shortcut.Shortcut{ Service: "chat", Command: "+chat-messages", Product: "chat", Description: "读取指定群聊或单聊的消息记录,支持有界全量分页与原子 JSON 导出", Intent: "当你要读取或导出一个指定群聊或单聊的消息记录时使用;--sender 是可选的姓名、userId 或 openDingTalkId 混合入口:姓名优先唯一解析,稳定 ID 精确路由;通讯录无法分类时仍按原值 userId 筛选并保留 identity_unverified,可交付精确命中但不能把原值升级为已验证身份或作完整否定结论。--sender-query 只按姓名唯一解析,解析失败会抑制未过滤消息并返回错误。不传发送者条件时原样读取会话且不查询发送者身份。sender 展示名不参与身份比较;" + "群聊的 --group 可传群名或 openConversationId,单聊可传 --user 或 --open-dingtalk-id,所有目标参数互斥且必须选一个。自然群名只在唯一解析后读取,多候选会返回结构化 candidates。" + "省略时间参数时默认从当前时间向前读取最近消息;兼容模式可用 --time/--direction,范围模式可用公开可选的 --start/--end/--order(兼容 --start-time/--end-time/--sort),范围语义为 [start,end)。" + "全量读取用 --page-all,并由 --page-limit/--max-items 保持有界;结果公开 complete、hasMore、nextPage、stopReason、截断和逐页失败,不能把部分结果称为完整。--output 把同一 ledger 原子写为工作目录内 JSON。" + "默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_chat_messages", CanonicalPath: "chat.shortcut_chat_messages", CLIPath: "chat +chat-messages", PrimaryCLIPath: "chat +chat-messages", }, Description: "读取指定群聊或单聊的消息记录,支持有界全量分页与原子 JSON 导出", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.", }, Selection: contract.SelectionSpec{ AgentSummary: "读取指定群聊或单聊的消息记录,支持有界全量分页与原子 JSON 导出", UseWhen: []string{"当你要读取或导出一个指定群聊或单聊的消息记录时使用;--sender 是可选的姓名、userId 或 openDingTalkId 混合入口:姓名优先唯一解析,稳定 ID 精确路由;通讯录无法分类时仍按原值 userId 筛选并保留 identity_unverified,可交付精确命中但不能把原值升级为已验证身份或作完整否定结论。--sender-query 只按姓名唯一解析,解析失败会抑制未过滤消息并返回错误。不传发送者条件时原样读取会话且不查询发送者身份。sender 展示名不参与身份比较;" + "群聊的 --group 可传群名或 openConversationId,单聊可传 --user 或 --open-dingtalk-id,所有目标参数互斥且必须选一个。自然群名只在唯一解析后读取,多候选会返回结构化 candidates。" + "省略时间参数时默认从当前时间向前读取最近消息;兼容模式可用 --time/--direction,范围模式可用公开可选的 --start/--end/--order(兼容 --start-time/--end-time/--sort),范围语义为 [start,end)。" + "全量读取用 --page-all,并由 --page-limit/--max-items 保持有界;结果公开 complete、hasMore、nextPage、stopReason、截断和逐页失败,不能把部分结果称为完整。--output 把同一 ledger 原子写为工作目录内 JSON。" + "默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。"}, AvoidWhen: []string{"以发送者、关键词、@对象或消息类型为主的直接条件检索优先使用 +search-msg;已有一批精确消息 ID 时使用 +messages-mget。已选择会话读取时可在同一次调用附带发送者姓名,不需要再搜索消息"}, Examples: []string{ "dws chat +chat-messages --group <openConversationId> --direction older", "dws chat +chat-messages --group <openConversationId> --direction older --jq '.messages[] | {messageId, text}'", }, }, }, Flags: append([]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: "open-conversation-id", Type: shortcut.FlagString, Desc: "--conversation-id 的兼容别名", Hidden: true}, {Name: "chat-query", Type: shortcut.FlagString, Desc: "按群名唯一解析目标会话(可选,与其他会话目标参数互斥)"}, {Name: "user", Type: shortcut.FlagString, Desc: "单聊对方的 userId,与 --group 互斥"}, {Name: "user-query", Type: shortcut.FlagString, Desc: "按姓名解析唯一 openDingTalkId 的兼容入口", Hidden: true}, {Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊对方的 openDingTalkId,与 --group/--user 互斥"}, {Name: "sender", Type: shortcut.FlagStringSlice, Desc: "单个或多个发送者姓名、userId 或 openDingTalkId;姓名唯一解析,稳定 ID 精确路由,通讯录无法分类时按原值 userId 筛选并保留身份未验证状态"}, {Name: "sender-query", Type: shortcut.FlagStringSlice, Desc: "显式按姓名唯一解析发送者的兼容入口;解析失败时抑制未过滤消息并返回错误(可选,可重复或逗号分隔)"}, {Name: "time", Type: shortcut.FlagString, Desc: "时间边界,如 \"2025-03-01 00:00:00\";--time 必须是 RFC3339、YYYY-MM-DD HH:mm:ss 或 YYYY-MM-DD;省略时从当前时间向前读取最近消息"}, {Name: "start", Type: shortcut.FlagString, Desc: "范围开始时间(可选、包含),支持 RFC3339、YYYY-MM-DD HH:mm:ss 或 YYYY-MM-DD"}, {Name: "start-time", Type: shortcut.FlagString, Desc: "--start 的 lark-cli 对齐别名(可选、包含)"}, {Name: "end", Type: shortcut.FlagString, Desc: "范围结束时间(可选、不包含);仅传开始时间时默认为当前时间"}, {Name: "end-time", Type: shortcut.FlagString, Desc: "--end 的 lark-cli 对齐别名(可选、不包含)"}, {Name: "order", Type: shortcut.FlagString, Enum: []string{"asc", "desc"}, Desc: "结果及范围遍历顺序 asc/desc(可选,默认 desc;asc 必须指定 --start/--start-time)"}, {Name: "sort", Type: shortcut.FlagString, Enum: []string{"asc", "desc"}, Desc: "--order 的 lark-cli 对齐别名(可选;asc 必须指定 --start/--start-time)"}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页拉取的消息条数;显式页大小必须大于 0"}, {Name: "size", Type: shortcut.FlagInt, Desc: "--limit 的旧版别名", Hidden: true}, {Name: "page-size", Type: shortcut.FlagInt, Desc: "--limit 的兼容别名", Hidden: true}, {Name: "direction", Type: shortcut.FlagString, Enum: []string{"newer", "older"}, Desc: "时间方向 newer/older;省略时为 older,从时间边界向前读取"}, {Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出消息 reaction(默认输出)"}, {Name: "page-all", Type: shortcut.FlagBool, Desc: "沿 typed nextPage.time 自动读取后续页;--page-limit 仅与 --page-all 一起使用且范围 1-500;--max-items 仅与 --page-all 一起使用且不能为负数;--max-results 仅与 --page-all 一起使用且不能为负数;--page-delay 仅与 --page-all 一起使用且不能为负数"}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, {Name: "max-items", Type: shortcut.FlagInt, Desc: "自动翻页最多返回条数(默认 0 表示不限制);--max-items 仅与 --page-all 一起使用且不能为负数"}, {Name: "max-results", Type: shortcut.FlagInt, Desc: "--max-items 的公开兼容别名;--max-results 仅与 --page-all 一起使用且不能为负数"}, {Name: "page-delay", Type: shortcut.FlagInt, Desc: "自动翻页每页之间等待毫秒数(默认 0 表示不等待);--page-delay 仅与 --page-all 一起使用且不能为负数"}, {Name: "output", Shorthand: "o", Type: shortcut.FlagString, Desc: "把完整结构化 ledger 原子写入工作目录内的相对 JSON 文件"}, }, chatshortcut.MessageResourceDownloadFlags()...), Constraints: append([]shortcut.Constraint{ {Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id", "id", "open-conversation-id", "chat-query", "user", "user-query", "open-dingtalk-id"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"limit", "size", "page-size"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"start", "start-time"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"end", "end-time"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"order", "sort"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"time", "start", "start-time"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"time", "end", "end-time"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"time", "order", "sort"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"sender", "sender-query"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"direction", "start", "start-time"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"direction", "end", "end-time"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"direction", "order", "sort"}}, {Kind: shortcut.ConstraintCustom, Flags: []string{"time"}, Description: "--time 必须是 RFC3339、YYYY-MM-DD HH:mm:ss 或 YYYY-MM-DD"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"order", "sort"}, Description: "asc 必须指定 --start/--start-time"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"limit"}, Description: "显式页大小必须大于 0"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "max-items"}, Description: "--max-items 仅与 --page-all 一起使用且不能为负数"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "max-results"}, Description: "--max-results 仅与 --page-all 一起使用且不能为负数"}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"max-items", "max-results"}}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-delay"}, Description: "--page-delay 仅与 --page-all 一起使用且不能为负数"}, { Kind: shortcut.ConstraintCustom, Flags: []string{"output", "overwrite"}, Description: "--output 必须是工作目录内的相对 JSON 文件;默认不覆盖,--overwrite 仅与 --output 一起使用", }, }, chatshortcut.MessageResourceDownloadConstraints()...), Tips: []string{ `dws chat +chat-messages --group <openconversation_id> --time "2025-03-01 00:00:00"`, `dws chat +chat-messages --group <openconversation_id> --start "2025-03-01T00:00:00+08:00" --end "2025-03-02T00:00:00+08:00" --order asc --page-all`, `dws chat +chat-messages --user <userId> --time "2025-03-01 00:00:00" --page-all --page-limit 50`, `dws chat +chat-messages --group <openconversation_id> --direction older --page-all --output ./exports/messages.json`, `dws chat +chat-messages --group <openconversation_id> --direction older --jq '.messages[] | {messageId, text}'`, }, Validate: validateChatMessages, Execute: executeChatMessages, }
ChatMessages resolves one conversation, projects messages into the shared typed result contract, and optionally follows bounded continuation pages, downloads resources, or atomically exports the complete ledger as JSON.
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_conflicts", CanonicalPath: "calendar.shortcut_conflicts", CLIPath: "calendar +conflicts", PrimaryCLIPath: "calendar +conflicts", }, Description: "检测我某天日程的时间冲突(重叠/双重预订,默认今天)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "检测我某天日程的时间冲突(重叠/双重预订,默认今天)", UseWhen: []string{"当你想快速知道『我今天(或某天)的日程有没有时间冲突、撞车的会议』时使用;内部自动算出目标日期的时间范围(默认今天,可用 --in-days 指定几天后),列出当天全部日程,再在本地两两比对开始/结束时间,找出所有时间段重叠的日程对并报告。只读操作,不修改任何日程;没有冲突时明确告诉你「无冲突」。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws calendar +conflicts", "dws calendar +conflicts --in-days 1", }, }, }, 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")) events, err := calendarSmartListAll(rt, 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 events { 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, "complete": true, }) }, }
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{ OutputRollout: output.RolloutUnifiedActive, Service: "todo", Command: "+created-todos", Product: "todo", Description: "列出我创建的待办(我作为创建人 creator 发起的待办,而非分配给我执行的)", Intent: "当你想快速看清『哪些待办是我自己创建/发起的』,而不是别人指派给我执行的待办时使用;" + "内部拉取你当前组织下角色为创建人(creator)的待办列表(roleTypes=[\"creator\"]," + "creator 是待办列表支持的角色枚举之一),再在本地把每条待办投影成标题(subject)、" + "任务 ID(taskId) 和截止时间(dueTime) 打印出来。这是纯只读操作,只做列表与投影," + "不会创建或修改任何待办;若没有你创建的待办则返回空列表。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "todo", Name: "shortcut_created_todos", CanonicalPath: "todo.shortcut_created_todos", CLIPath: "todo +created-todos", PrimaryCLIPath: "todo +created-todos", }, Description: "列出我创建的待办(我作为创建人 creator 发起的待办,而非分配给我执行的)", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"我创建的全部待办","properties":{"created":{"type":"array","description":"创建人角色待办","items":{"type":"object","description":"待办条目","additionalProperties":true}}},"required":["created"],"additionalProperties":false}`)}, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出我创建的待办(我作为创建人 creator 发起的待办,而非分配给我执行的)", UseWhen: []string{"当你想快速看清『哪些待办是我自己创建/发起的』,而不是别人指派给我执行的待办时使用;内部拉取你当前组织下角色为创建人(creator)的待办列表(roleTypes=[\"creator\"],creator 是待办列表支持的角色枚举之一),再在本地把每条待办投影成标题(subject)、任务 ID(taskId) 和截止时间(dueTime) 打印出来。这是纯只读操作,只做列表与投影,不会创建或修改任何待办;若没有你创建的待办则返回空列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws todo +created-todos"}, }, }, Flags: []shortcut.Flag{}, Tips: []string{ `dws todo +created-todos`, }, Execute: func(rt *shortcut.RuntimeContext) error { cards, err := shortcutListAllTodoCards(rt, map[string]any{ "roleTypes": []string{"creator"}, }) if err != nil { return err } 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: "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", Intent: "当你只知道对方姓名、想直接发一条单聊消息而不想先查 userId 时使用;" + "内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_dm", CanonicalPath: "chat.shortcut_dm", CLIPath: "chat +dm", PrimaryCLIPath: "chat +dm", }, Description: "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", UseWhen: []string{"当你只知道对方姓名、想直接发一条单聊消息而不想先查 userId 时使用;内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +dm --to 张三 --content \"周报发我一下\""}, }, Parameters: []contract.ParamDecl{renamedRequiredParam("content", "text")}, }, Flags: []shortcut.Flag{ {Name: "to", Type: shortcut.FlagString, Desc: "收件人姓名/花名", Required: true}, {Name: "content", Type: shortcut.FlagString, Desc: "消息内容(支持 Markdown)", Required: true, Aliases: []string{"text"}}, shortcut.AIMessageTagFlag(), }, Tips: []string{`dws chat +dm --to 张三 --content "周报发我一下"`}, Execute: func(rt *shortcut.RuntimeContext) error { text := rt.StrFirst("text", "content") resolved, err := targetresolver.ResolveUser( rt, rt.Str("to"), targetresolver.IdentityOpenDingTalkID, ) if err != nil { return err } return chatshortcut.ExecuteResolvedUserMarkdown( rt, chatshortcut.ResolvedUserMessageTarget{ OpenDingTalkID: resolved.Selected.OpenDingTalkID, }, text, ) }, }
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 张三 --content "周报发我一下"
var DeptMembers = shortcut.Shortcut{ Service: "contact", Command: "+dept-members", Product: "contact", Description: "按部门名列出部门成员(自动解析 deptId)", Intent: "当你只知道某个部门的名称、想知道该部门里都有哪些成员时使用;" + "内部先按部门名搜索出唯一的 deptId(匹配到 0 个或多个时不猜测," + "而是列出候选部门名与 deptId 让你选),再打印该部门的直接成员列表" + "(仅本部门,不递归下级部门)。只读,不做任何修改。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "shortcut_dept_members", CanonicalPath: "contact.shortcut_dept_members", CLIPath: "contact +dept-members", PrimaryCLIPath: "contact +dept-members", }, Description: "按部门名列出部门成员(自动解析 deptId)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按部门名列出部门成员(自动解析 deptId)", UseWhen: []string{"当你只知道某个部门的名称、想知道该部门里都有哪些成员时使用;内部先按部门名搜索出唯一的 deptId(匹配到 0 个或多个时不猜测,而是列出候选部门名与 deptId 让你选),再打印该部门的直接成员列表(仅本部门,不递归下级部门)。只读,不做任何修改。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws contact +dept-members --dept 技术部"}, }, }, 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, err := strictDeptCandidates(data, "contact/search_dept_by_keyword") if err != nil { return err } 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), "、"))) } data, err = rt.CallMCPData("contact", "get_dept_members_by_deptId", map[string]any{ "deptIds": []string{strconv.FormatInt(depts[0].id, 10)}, }) if err != nil { return err } members, err := strictContactMembers(data, "contact/get_dept_members_by_deptId") if err != nil { return err } return rt.Output(map[string]any{"count": len(members), "members": members}) }, }
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, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_doc_append", CanonicalPath: "doc.shortcut_doc_append", CLIPath: "doc +doc-append", PrimaryCLIPath: "doc +doc-append", }, Description: "在文档末尾追加一段文本(安全追加,不改动原有内容)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "在文档末尾追加一段文本(安全追加,不改动原有内容)", UseWhen: []string{"当你只想往一篇钉钉文档的最后面补一段文字、又不想动原有内容时使用;内部用文档更新的“追加(append)”模式,把你给的文本安全地拼到文档末尾,不需要你先去查文档块列表、算末尾位置或手工拼块结构。会真实写入文档内容。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws doc +doc-append --doc DOC_ID --content \"补充说明:本方案已评审通过。\"", "dws doc +doc-append --doc \"https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>\" --content \"追加一行备注\"", }, }, Parameters: []contract.ParamDecl{renamedRequiredParam("content", "text")}, }, Flags: []shortcut.Flag{ {Name: "doc", Type: shortcut.FlagString, Desc: "文档 documentId / nodeId(或文档 URL/token)", Required: true}, {Name: "content", Type: shortcut.FlagString, Desc: "要追加到文档末尾的文本", Required: true, Aliases: []string{"text"}}, }, Tips: []string{ `dws doc +doc-append --doc DOC_ID --content "补充说明:本方案已评审通过。"`, `dws doc +doc-append --doc "https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>" --content "追加一行备注"`, }, Execute: func(rt *shortcut.RuntimeContext) error { nodeID := strings.TrimSpace(rt.Str("doc")) if nodeID == "" { return apperrors.NewValidation("--doc 不能为空,请提供文档 documentId/nodeId 或文档 URL") } text := rt.StrFirst("text", "content") if strings.TrimSpace(text) == "" { return apperrors.NewValidation("--content 不能为空,请提供要追加的文本") } if runes := utf8.RuneCountInString(text); runes > helpers.DefaultMarkdownChunkRunes { return apperrors.NewValidation( fmt.Sprintf("--text 长度 %d 字符超过单次写入上限 %d;本命令只用于追加一小段文本,不做自动分片", runes, helpers.DefaultMarkdownChunkRunes), apperrors.WithReason("doc_append_content_too_long"), apperrors.WithRetryable(false), apperrors.WithActions( "改用 dws doc +update --command append --content @文件 —— 它会自动分片、重发表头并回读校验", "或自行把文本拆成多段,分多次 +doc-append", ), ) } 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 --content "今天的会议结论:下周一上线。"
var DueToday = shortcut.Shortcut{ OutputRollout: output.RolloutUnifiedActive, 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ProductID: "todo", Name: "shortcut_due_today", CanonicalPath: "todo.shortcut_due_today", CLIPath: "todo +due-today", PrimaryCLIPath: "todo +due-today"}, Description: "列出我今天到期的待办", Interface: &contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "Reviewed Todo composite: local-day window construction, strict full pagination and projection are owned by the executable shortcut."}, Selection: contract.SelectionSpec{ AgentSummary: "列出我今天到期的待办", UseWhen: []string{"需要查看本地日历日今天到期的待办时"}, AvoidWhen: []string{"查看已经逾期的任务时使用 todo +overdue"}, Examples: []string{"dws todo +due-today"}, }, Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"今天到期的待办","properties":{"count":{"type":"integer","description":"任务数量"},"tasks":{"type":"array","description":"今天到期的任务","items":{"type":"object","description":"待办条目","additionalProperties":true}}},"required":["count","tasks"],"additionalProperties":false}`)}, }, 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 { due, ok := shortcutOverdueDueTime(m) if !ok { return shortcutTodoResponseError("malformed_due_time", "今天到期结果缺少可解析的 dueTime") } if due < startMs || due >= endMs { continue } taskID := shortcutRelatedTaskID(m) results = append(results, shortcutRelatedProject(m, taskID)) } 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_find_doc", CanonicalPath: "doc.shortcut_find_doc", CLIPath: "doc +find-doc", PrimaryCLIPath: "doc +find-doc", }, Description: "按关键词搜索云文档并投影关键字段(只读)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按关键词搜索云文档并投影关键字段(只读)", UseWhen: []string{"当你只记得云文档标题或内容里的某个关键词,想快速按关键词找到匹配的文档、拿到它的标题、URL、类型和 token 以便后续查看或编辑,却不想拿到一大坨原始字段时使用;内部调用云文档的 search_documents 工具,把 --query 作为搜索关键词(keyword),可选地用 --limit 限制返回条数(pageSize),再在本地把每条命中结果精简为「标题、URL、类型、token」四个字段后打印。这是纯只读操作,只做搜索与本地投影,不会创建、修改或删除任何文档;未命中时提示「没搜到文档」。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws doc +find-doc --query 季度汇报", "dws doc +find-doc --query 合同 --limit 10", }, }, }, 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "drive", Name: "shortcut_find_file", CanonicalPath: "drive.shortcut_find_file", CLIPath: "drive +find-file", PrimaryCLIPath: "drive +find-file", }, Description: "按名称关键词搜索钉盘文件并投影关键字段(只读)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按名称关键词搜索钉盘文件并投影关键字段(只读)", UseWhen: []string{"当你只记得钉盘文件的名字(或其中一部分),想快速按文件名关键词找到它、拿到它的 dentryId 以便后续下载/查看,却不想手动翻目录或写复杂过滤条件时使用;内部调用钉盘的 search_files 工具,把 --query 作为文件名关键词(keyword) 并限定搜索范围为钉盘文件(searchTarget=file),再在本地把每条命中结果精简为「文件名、类型、dentryId、大小」四个字段后打印。这是纯只读操作,只做搜索与本地投影,不会创建、移动或删除任何文件;未命中时返回空列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws drive +find-file --query 季度汇报", "dws drive +find-file --query 合同", }, }, }, 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "mail", Name: "shortcut_find_mail_user", CanonicalPath: "mail.shortcut_find_mail_user", CLIPath: "mail +find-mail-user", PrimaryCLIPath: "mail +find-mail-user", }, Description: "按关键词搜索邮箱联系人并投影列表(姓名/昵称/邮箱/工号等)", Parameters: []contract.ParamDecl{ {Name: "query", Property: "query"}, {Name: "limit", Property: "limit"}, {Name: "cursor", Property: "cursor"}, }, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按关键词搜索邮箱联系人并投影列表(姓名/昵称/邮箱/工号等)", UseWhen: []string{"当你只知道某人的姓名、花名或邮箱片段,想在企业邮箱通讯录里按关键词把匹配的邮箱用户找出来、并只看一份精简清单(姓名、昵称、邮箱地址、工号、职位、工作地)而不想拿到一大坨原始字段时使用;内部按 --query 关键词调用邮箱用户搜索,再在本地把每个匹配用户投影成整洁记录打印出来,可配合 --format/--jq/--fields。这是纯只读操作,只做搜索与本地投影,不会修改任何数据;注意仅企业邮箱可用(个人邮箱如 xxx@dingtalk.com 会因无权限报错);若没有命中则提示「没搜到邮箱联系人」。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws mail +find-mail-user --query \"张三\"", "dws mail +find-mail-user --query alice --limit 10", }, }, }, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词(姓名/花名/邮箱片段,必填且不能为空)", Required: true}, {Name: "limit", Type: shortcut.FlagInt, Desc: "返回条数上限(可选,1-100)", Required: false}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,取自上一页 nextCursor", Required: false}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"query"}, Description: "不能为空"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"limit"}, Description: "1-100"}, }, Validate: func(rt *shortcut.RuntimeContext) error { if err := smartMailValidateRequiredText(rt, "query"); err != nil { return err } return smartMailValidatePageSize(rt, "limit", 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")) } if rt.Changed("cursor") { args["cursor"] = rt.Str("cursor") } data, err := rt.CallMCPData("mail", "search_mail_users", args) if err != nil { return err } users, err := smartMailCollection(data, "mail/search_mail_users", "users") if err != nil { return err } results := make([]map[string]any, 0, len(users)) for _, u := range users { projected, err := findMailUserProjection(u) if err != nil { return err } results = append(results, projected) } complete, next, err := smartMailPage(data, "mail/search_mail_users", "", rt.Str("cursor")) if err != nil { return err } return smartMailOutputPage(rt, "users", results, complete, next) }, }
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "aitable", Name: "shortcut_find_record", CanonicalPath: "aitable.shortcut_find_record", CLIPath: "aitable +find-record", PrimaryCLIPath: "aitable +find-record", }, Description: "在指定多维表里按关键词查记录(只读)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "在指定多维表里按关键词查记录(只读)", UseWhen: []string{"当你已经知道某个多维表的 baseId 和 tableId、想按一个关键词快速找出匹配的行记录,却不想手写结构化过滤条件时使用;内部直接调用 query_records,把 --query 作为全文关键词(keyword)在该表里检索并打印匹配记录。不传 --query 时则返回该表的前若干条记录。这是只读操作,不会修改任何数据。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws aitable +find-record --base B --table T", "dws aitable +find-record --base B --table T --query 张三", }, }, }, 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_free", CanonicalPath: "calendar.shortcut_free", CLIPath: "calendar +free", PrimaryCLIPath: "calendar +free", }, Description: "按姓名查询某人在指定时间段内的忙闲状态(自动解析 userId)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名查询某人在指定时间段内的忙闲状态(自动解析 userId)", UseWhen: []string{"当你只知道对方姓名、想知道 TA 在某段时间内是空闲还是被日程占用(比如约会前先看看有没有空)而不想先手动查 userId 时使用;内部先按姓名搜通讯录解析出唯一 userId,姓名匹配到多人时会列出候选让你区分,再按时间范围查询忙闲。只读,不产生任何日程变更。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws calendar +free --who 张三 --start \"2026-03-10T14:00:00+08:00\" --end \"2026-03-10T18:00:00+08:00\""}, }, }, 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, err := calendarSmartBusySlots(data) if err != nil { return err } 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_free_slots", CanonicalPath: "calendar.shortcut_free_slots", CLIPath: "calendar +free-slots", PrimaryCLIPath: "calendar +free-slots", }, Description: "找我某天工作时段内的空闲时间段(默认今天 09:00-18:00)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "找我某天工作时段内的空闲时间段(默认今天 09:00-18:00)", UseWhen: []string{"当你想知道『我今天(或某天)还有哪些时间是空的、可以安排会议/事情』时使用;内部列出目标日期(默认今天,--in-days 指定几天后)的全部日程,合并忙碌时段,再在工作时间范围内(默认 09:00-18:00,可用 --from/--to 指定起止小时)算出所有空闲窗口并给出每段的起止与时长。只读操作,不修改任何日程;用于快速定位可预约的空档。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws calendar +free-slots", "dws calendar +free-slots --in-days 1 --from 9 --to 20", }, }, }, 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) events, err := calendarSmartListAll(rt, 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 events { 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, "complete": true, }) }, }
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
Service: "doc", Command: "+grant-and-share", Product: "doc", Description: "确保目标角色后按姓名逐人发送文档链接", Intent: "当用户要确保多人获得指定文档角色后再私信链接时使用;缺少权限时授权、角色不足时升级,无法识别当前角色则停止,再只向权限已经足够的人发送。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"}, Contract: docSmartContract("+grant-and-share", "确保目标角色后按姓名逐人发送文档链接", "当用户要确保多人获得指定文档角色后再私信链接时使用;缺少权限时授权、角色不足时升级,无法识别当前角色则停止,再只向权限已经足够的人发送。", []string{`dws doc +grant-and-share --node <DOC_ID> --url https://alidocs.dingtalk.com/i/nodes/<DOC_ID> --to 张三,李四 --role READER`}, false), Flags: append(permissionFlags(true), shortcut.Flag{Name: "url", Type: shortcut.FlagString, Desc: "文档链接", Required: true}, shortcut.Flag{Name: "note", Type: shortcut.FlagString, Desc: "附言"}, shortcut.AIMessageTagFlag(), ), Tips: []string{`dws doc +grant-and-share --node <DOC_ID> --url https://alidocs.dingtalk.com/i/nodes/<DOC_ID> --to 张三,李四 --role READER`}, Execute: executeGrantAndShare, }
var GroupMembers = shortcut.Shortcut{ Service: "chat", Command: "+group-members", Product: "chat", Description: "按群名唯一解析后全量列出用户成员并公开分页完整性", Intent: "当你只知道群的名字、想看看这个群里有哪些成员,而不想先手动查群 ID 时使用;" + "内部先按群名搜索群聊解析出唯一 openConversationId,再拉取该群的成员列表。" + "群名匹配到多个群时会列出候选让你区分、绝不自行假定。用户成员会自动翻页、稳定 ID 去重,并公开 complete/hasMore/nextCursor;--page-limit 保证有界。只读,不改动任何数据。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_group_members", CanonicalPath: "chat.shortcut_group_members", CLIPath: "chat +group-members", PrimaryCLIPath: "chat +group-members", }, Description: "按群名唯一解析后全量列出用户成员并公开分页完整性", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按群名唯一解析后全量列出用户成员并公开分页完整性", UseWhen: []string{"当你只知道群的名字、想看看这个群里有哪些成员,而不想先手动查群 ID 时使用;内部先按群名搜索群聊解析出唯一 openConversationId,再拉取该群的成员列表。群名匹配到多个群时会列出候选让你区分、绝不自行假定。用户成员会自动翻页、稳定 ID 去重,并公开 complete/hasMore/nextCursor;--page-limit 保证有界。只读,不改动任何数据。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +group-members --group 项目冲刺"}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群名称(搜群关键词,用群名里连续的核心词)", Required: true}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "最大用户成员页数;--page-limit 必须在 1-500 之间"}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"page-limit"}, Description: "--page-limit 必须在 1-500 之间"}, }, Tips: []string{`dws chat +group-members --group 项目冲刺`}, Validate: validateGroupMembersPageLimit, Execute: func(rt *shortcut.RuntimeContext) error { resolved, err := targetresolver.ResolveChat(rt, rt.Str("group")) if err != nil { return err } result, err := collectGroupUserMembers(rt, resolved.Selected.OpenConversationID, rt.Int("page-limit")) if err != nil { return err } return rt.Output(result.payload(resolved.Selected.OpenConversationID)) }, }
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, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_invite", CanonicalPath: "calendar.shortcut_invite", CLIPath: "calendar +invite", PrimaryCLIPath: "calendar +invite", }, Description: "按姓名把参会人加入已有日程(自动解析 userId 后批量添加)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名把参会人加入已有日程(自动解析 userId 后批量添加)", UseWhen: []string{"当你已经有一个日程(知道 eventId),想按姓名把几位同事拉进来当参会人时使用;内部先把 --with 里每个姓名解析成唯一 userId,再一次性把他们全部加到 --event 指定的日程里。会真实修改日程并发出参会邀请。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws calendar +invite --event EVENT_ID --with 张三", "dws calendar +invite --event EVENT_ID --with 张三,李四", }, }, }, 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 var userNames []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) userNames = append(userNames, user.name) } if len(userIDs) == 0 { return apperrors.NewValidation("--with 需要至少一个有效的参会人姓名") } preflight, err := rt.CallMCPData("calendar", "get_calendar_detail", map[string]any{"eventId": eventID}) if err != nil { return err } if _, err := calendarSmartRequireEvent(preflight, "calendar/get_calendar_detail", eventID); err != nil { return err } if rt.DryRun() { return rt.Output(map[string]any{ "success": true, "dryRun": true, "executed": false, "eventId": eventID, "inviteeCount": len(userIDs), }) } written, err := rt.CallMCPWriteDataStrict("calendar", "add_calendar_participant", map[string]any{ "eventId": eventID, "attendeesToAdd": userIDs, }) if err != nil { return err } if err := calendarSmartWriteReceipt(written, "calendar/add_calendar_participant"); err != nil { return err } participants, err := rt.CallMCPData("calendar", "get_calendar_participants", map[string]any{"eventId": eventID}) if err != nil { return err } present, err := calendarSmartAttendees(participants) if err != nil { return err } currentUserID, err := calendarSmartCurrentUserID(rt, present) if err != nil { return err } if err := calendarSmartVerifyAttendees(present, userIDs, userNames, currentUserID); err != nil { return err } return rt.Output(map[string]any{ "success": true, "eventId": eventID, "invitedCount": len(userIDs), "verified": true, }) }, }
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", Aliases: []string{"+latest-minutes"}, Product: "minutes", Description: "取我最新的一条妙记(听记)详情", Intent: "当你只想快速看回自己最近的一条会议听记,却不想先翻列表、复制 taskUuid 再查详情时使用;" + "内部先列出你创建的听记(可用 --keyword 缩小范围),自动挑出最新的一条,再拉取它的基础信息(标题、创建人、时间、访问链接等)。" + "这是只读操作,不会修改任何听记;若你名下没有任何听记则提示「暂无妙记」。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: minutesSmartContract( "+latest", "取我最新的一条妙记(听记)详情", "需要按真实创建/开始时间选出当前用户最新一条听记并直接读取基础详情时使用;可用 --keyword 缩小候选集。", []string{"已知 taskUuid 时直接使用基础信息原子命令;需要搜索多条结果时使用 +search"}, []string{"dws minutes +latest", "dws minutes +latest --keyword 周会"}, []string{"+latest-minutes"}, ), Flags: []shortcut.Flag{ {Name: "keyword", Type: shortcut.FlagString, Desc: "按关键字过滤听记(可选)", Required: false}, }, Tips: []string{ `dws minutes +latest`, `dws minutes +latest --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, err := latestMinutesTaskUUID(data) if err != nil { return err } if taskUUID == "" { return apperrors.NewValidation("暂无妙记") } basicData, err := rt.CallMCPData("minutes", "get_minutes_basic_info", map[string]any{ "taskUuid": taskUUID, }) if err != nil { return err } basic, err := minutesdata.Basic(taskUUID, basicData) if err != nil { return err } return rt.Output(basic) }, }
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 by an explicit comparable timestamp 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 dws minutes +latest --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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "aitable", Name: "shortcut_list_tables", CanonicalPath: "aitable.shortcut_list_tables", CLIPath: "aitable +list-tables", PrimaryCLIPath: "aitable +list-tables", }, Description: "列出某个多维表(base)里的所有数据表(只读,投影 tableId/tableName)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出某个多维表(base)里的所有数据表(只读,投影 tableId/tableName)", UseWhen: []string{"当你已经知道某个多维表(base)的 baseId、想一步看清这个 base 下都有哪些数据表(table)、拿到它们的 tableId 和 tableName 以便后续查记录或改结构,却不想手动翻 base get 的完整目录时使用;内部直接调用 get_tables,只传 baseId(不带 tableIds,因此返回该 base 下的全部数据表),再在本地把每张表投影成 tableId、tableName 两个关键字段打印出来。这是纯只读操作,只做列举与本地投影,不会创建、修改或删除任何表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws aitable +list-tables --base B"}, }, }, 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "shortcut_lookup", CanonicalPath: "contact.shortcut_lookup", CLIPath: "contact +lookup", PrimaryCLIPath: "contact +lookup", }, Description: "按姓名查询某人的完整资料(自动解析 userId 后取详情)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名查询某人的完整资料(自动解析 userId 后取详情)", UseWhen: []string{"当你只知道对方姓名、想一步拿到其完整资料(部门、职位、联系方式等)而不想先搜 userId 再查详情时使用;内部先按姓名搜通讯录解析出唯一 userId,再取详情,姓名匹配到多人时会列出候选让你区分。只读操作。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws contact +lookup --name 张三"}, }, }, 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 := strictResolveContactUser(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 } profile, err := strictUserDetail(data, user.userID, "contact/get_user_info_by_user_ids") if err != nil { return err } return rt.Output(map[string]any{"profile": profile}) }, }
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: "当你已有一个或最多 50 个 taskUuid,要一次读取所选 basic/summary/keywords/transcript/todos 产物时使用;" + "逐字稿默认追完所有分页并可用 file/both 安全落盘,批量结果带 complete 与逐项 failure ledger。" + "任一所选产物失败都会保留已取得结果并返回非零,绝不会把 partial bundle 当成完整成功;这是纯只读操作。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "minutes", Name: "shortcut_detail", CanonicalPath: "minutes.shortcut_detail", CLIPath: "minutes +detail", PrimaryCLIPath: "minutes +detail", }, Description: "批量聚合听记基础信息、摘要、关键词、完整逐字稿和行动项,支持安全文件输出", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "一条命令聚合取一条妙记(听记)的多项产物(基础信息/摘要/关键词/逐字稿/待办)", UseWhen: []string{"当你已经有某条听记的 taskUuid,想在一次操作里同时拿到它的基础信息、AI 摘要、关键词、逐字稿和待办,而不想分别敲 4~5 个子命令再自己拼时使用;内部按 --artifacts 选择要拉的产物(默认全部:basic/summary/keywords/transcript/todos),逐个调用对应的原子工具并聚合成一个结果,某一项失败不会中断整体(会以错误字符串记录在该项下)。这是纯只读操作,不会修改听记;--direction 仅影响逐字稿排序(0=正序默认,1=倒序)。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws minutes +detail --id <taskUuid>", "dws minutes +detail --ids <uuid1,uuid2> --artifacts summary,transcript --transcript-output file", }, }, Result: minutesDetailResult(), }, Flags: []shortcut.Flag{ {Name: "id", Type: shortcut.FlagString, Desc: "单个听记 taskUuid"}, {Name: "ids", Type: shortcut.FlagStringSlice, Desc: "多个听记 taskUuid,最多 50 个"}, {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"}}, {Name: "cursor", Type: shortcut.FlagString, Desc: "逐字稿单页/续拉的起始 nextToken"}, {Name: "single-page", Type: shortcut.FlagBool, Desc: "逐字稿只读取一页并返回 nextToken"}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "100", Desc: "逐字稿自动翻页安全上限"}, {Name: "transcript-output", Type: shortcut.FlagString, Default: "inline", Desc: "逐字稿输出方式", Enum: []string{"inline", "file", "both"}}, {Name: "output-dir", Type: shortcut.FlagString, Default: "minutes-transcripts", Desc: "file/both 模式下的安全相对输出目录"}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"id", "ids"}, Description: "--id 与 --ids 只能选择一种,去重后 taskUuid 必须为 1..50 个"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-limit"}, Description: "--page-limit 必须大于 0"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"transcript-output", "output-dir"}, Description: "file/both 输出目录必须是安全相对路径"}, }, Tips: []string{ `dws minutes +detail --id <taskUuid>`, `dws minutes +detail --ids <uuid1,uuid2> --artifacts summary,transcript --transcript-output file`, `dws minutes +detail --id <taskUuid> --direction 1 --transcript-output both`, }, Validate: func(rt *shortcut.RuntimeContext) error { if rt.Int("page-limit") <= 0 { return apperrors.NewValidation("--page-limit 必须大于 0") } if strings.TrimSpace(rt.Str("id")) != "" && len(rt.StrSlice("ids")) > 0 { return apperrors.NewValidation("--id 与 --ids 只能选择一种") } ids := minutesDetailIDs(rt) if len(ids) == 0 || len(ids) > 50 { return apperrors.NewValidation("taskUuid 数量必须为 1..50") } if rt.Str("transcript-output") != "inline" { return localio.ValidateOutput(rt.Str("output-dir")) } return nil }, Execute: func(rt *shortcut.RuntimeContext) error { ids := minutesDetailIDs(rt) direction := rt.Str("direction") if direction == "" { direction = "0" } want := rt.StrSlice("artifacts") if len(want) == 0 { want = minutesArtifactOrder } results := make([]map[string]any, 0, len(ids)) allFailures := make([]map[string]any, 0) succeeded := 0 for _, taskUUID := range ids { bundle, failures := readMinutesDetail(rt, taskUUID, want, direction) results = append(results, bundle) if len(failures) == 0 { succeeded++ } for _, failure := range failures { failure["taskUuid"] = taskUUID allFailures = append(allFailures, failure) } } if !rt.Changed("ids") && len(results) == 1 { if err := rt.Output(results[0]); err != nil { return err } if len(allFailures) > 0 { return minutesDetailReadError(ids, allFailures) } return nil } payload := map[string]any{ "operation": "minutes.detail", "complete": len(allFailures) == 0, "requested": len(ids), "succeeded": succeeded, "failed": len(ids) - succeeded, "results": results, "failures": allFailures, } if err := rt.Output(payload); err != nil { return err } if len(allFailures) > 0 { return minutesDetailReadError(ids, allFailures) } return nil }, }
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "minutes", Name: "shortcut_minutes_search", CanonicalPath: "minutes.shortcut_minutes_search", CLIPath: "minutes +minutes-search", PrimaryCLIPath: "minutes +minutes-search", }, Description: "按关键词搜索我的妙记并投影列表", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按关键词搜索我的妙记并投影列表", UseWhen: []string{"当你想按关键词快速找回自己创建的会议听记(妙记),只需要看到匹配到的标题、创建时间和 taskUuid 列表、而不想拿到一大坨原始字段时使用;内部按 --query 关键词列出你创建的听记(最多 20 条),再在本地投影出每条的标题、创建时间和 taskUuid。这是纯只读操作,只做搜索与本地投影,不会修改任何听记;若没有匹配的听记则提示「没搜到妙记」。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws minutes +minutes-search --query 周会"}, }, }, 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 } page, err := minutesdata.ParseListPage(data) if err != nil { return err } results, err := minutesdata.ProjectList(page) if err != nil { return err } return rt.Output(map[string]any{"count": len(results), "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{ OutputRollout: output.RolloutLegacyOnly, Service: "attendance", Command: "+my-attendance", Product: "attendance", Description: "查我今天的考勤打卡记录(打卡流水,自动解析当前用户)", Intent: "当你想快速看自己今天的打卡流水(几点上下班打卡、打卡地址/定位方式)、又不想先查自己的 userId " + "再手动填写今天的起止时间时使用;内部先取当前登录用户的 userId,再按本地时区算出今天 00:00 到次日 00:00 的时间窗," + "最后查询你今天的打卡流水记录。只读操作,不会修改任何考勤数据;今天若还没有任何打卡则返回空结果。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "attendance", Name: "shortcut_my_attendance", CanonicalPath: "attendance.shortcut_my_attendance", CLIPath: "attendance +my-attendance", PrimaryCLIPath: "attendance +my-attendance", }, Description: "查我今天的考勤打卡记录(打卡流水,自动解析当前用户)", Interface: &contract.InterfaceSpec{ Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceAvailable, Reason: "Historical executable Schema compatibility: this command remains callable, but the reviewed Shortcut catalog keeps it non-public until a known-nonempty current-day fixture closes the false-empty proof gap.", }, Selection: contract.SelectionSpec{ AgentSummary: "查我今天的考勤打卡记录(打卡流水,自动解析当前用户)", UseWhen: []string{"当你想快速看自己今天的打卡流水(几点上下班打卡、打卡地址/定位方式)、又不想先查自己的 userId 再手动填写今天的起止时间时使用;内部先取当前登录用户的 userId,再按本地时区算出今天 00:00 到次日 00:00 的时间窗,最后查询你今天的打卡流水记录。只读操作,不会修改任何考勤数据;今天若还没有任何打卡则返回空结果。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws attendance +my-attendance"}, }, }, 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 := strictAttendanceCurrentUserID(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 outputStrictAttendanceRecords(rt, 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_my_free", CanonicalPath: "calendar.shortcut_my_free", CLIPath: "calendar +my-free", PrimaryCLIPath: "calendar +my-free", }, Description: "查我自己在某时间段的忙闲(默认今天,无需输入姓名)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "查我自己在某时间段的忙闲(默认今天,无需输入姓名)", UseWhen: []string{"当你(或 AI agent)想知道『我自己什么时候有空/忙』、用于安排会议或回复邀约时使用;不用像 +free 那样传别人的姓名——内部自动解析当前用户的 userId,再查其忙闲时段。默认查今天(本地时区 00:00 到次日 00:00),也可用 --start/--end 指定 ISO8601 时间范围。只读操作,只查忙闲、不创建或修改任何日程;返回按时间排列的忙碌时段,空则表示这段时间全空。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []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", }, }, }, 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, err := calendarSmartBusySlots(data) if err != nil { return err } 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 through the shared strict busy-response parser.
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_my_groups", CanonicalPath: "chat.shortcut_my_groups", CLIPath: "chat +my-groups", PrimaryCLIPath: "chat +my-groups", }, Description: "列出我加入的群,可按类型过滤并投影关键字段", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出我加入的群,可按类型过滤并投影关键字段", UseWhen: []string{"当你想快速看一眼自己都加入了哪些群、以及每个群的会话ID、名称、群主和人数,而不想翻分页或盯着原始返回时使用;内部分页拉取你加入的群列表,把每个群防御式地投影成 会话id / 名称 / 群主 / 人数 / 类型 等关键字段,输出成干净的结果。可选 --type 在本地按群类型过滤(底层接口本身不带类型参数,故为客户端过滤)。这是只读操作,不会改动任何群或成员关系。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws chat +my-groups", "dws chat +my-groups --type group", }, }, }, Flags: append([]shortcut.Flag{ {Name: "type", Type: shortcut.FlagString, Desc: "按群类型过滤(可选,如返回中的 groupType/conversationType,大小写不敏感)", Required: false}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页返回数量(默认 200);--limit 必须在 1-200 之间", Default: "200"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传上次的 nextCursor"}, {Name: "page-all", Type: shortcut.FlagBool, Desc: "沿 nextCursor 自动读取全部已加入群;--page-limit 仅与 --page-all 一起使用且范围 1-500;--max-items/--page-delay 仅与 --page-all 一起使用;值必须大于等于 0"}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, }, shortcut.AutoPageControlFlags()...), Constraints: append([]shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"limit"}, Description: "--limit 必须在 1-200 之间"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, }, shortcut.AutoPageControlConstraints()...), Tips: []string{ `dws chat +my-groups`, `dws chat +my-groups --type group`, `dws chat +my-groups --page-all --page-limit 50`, }, Validate: validateMyGroups, Execute: executeMyGroups, }
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 NextEvent = shortcut.Shortcut{ Service: "calendar", Command: "+next-event", Product: "calendar", Description: "查看接下来最近的一个日程(默认扫描未来 7 天)", Intent: "当你只想知道『我下一个日程是什么、什么时候开始』、而不想翻一整份日程列表时使用;" + "内部以当前时间为起点、往后 7 天为范围,拉取主日历下的日程," + "按开始时间升序挑出最近的那一个并打印摘要(标题、开始/结束时间、地点)。" + "若这 7 天内没有任何日程,会明确提示『近 7 天无日程』。只读,不做任何修改。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_next_event", CanonicalPath: "calendar.shortcut_next_event", CLIPath: "calendar +next-event", PrimaryCLIPath: "calendar +next-event", }, Description: "查看接下来最近的一个日程(默认扫描未来 7 天)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "查看接下来最近的一个日程(默认扫描未来 7 天)", UseWhen: []string{"当你只想知道『我下一个日程是什么、什么时候开始』、而不想翻一整份日程列表时使用;内部以当前时间为起点、往后 7 天为范围,拉取主日历下的日程,按开始时间升序挑出最近的那一个并打印摘要(标题、开始/结束时间、地点)。若这 7 天内没有任何日程,会明确提示『近 7 天无日程』。只读,不做任何修改。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws calendar +next-event"}, }, }, 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(), } events, err := calendarSmartListAll(rt, params) if err != nil { return err } event := shortcutNextEventPick(events, now) if event == nil { return rt.Output(map[string]any{"event": nil, "message": "近 7 天无日程", "complete": true}) } return rt.Output(map[string]any{"event": shortcutNextEventProject(event), "complete": true}) }, }
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "shortcut_org", CanonicalPath: "contact.shortcut_org", CLIPath: "contact +org", PrimaryCLIPath: "contact +org", }, Description: "按姓名查某人所在部门的详情(自动解析 userId 与 deptId)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名查某人所在部门的详情(自动解析 userId 与 deptId)", UseWhen: []string{"当你只知道某位同事的姓名、想知道 TA 所在部门(部门ID、名称、人数)时使用;内部先按姓名解析出唯一 userId,再取 TA 的组织信息拿到主部门 deptId,最后打印该部门的详情。只读,不做任何修改。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws contact +org --name 张三"}, }, }, 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 := strictResolveContactUser(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 } profile, err := strictUserDetail(data, user.userID, "contact/get_user_info_by_user_ids") if err != nil { return err } deptID, err := strictPrimaryDeptID(profile, "contact/get_user_info_by_user_ids") if err != nil { return err } data, err = rt.CallMCPData("contact", "get_dept_info_by_dept_id", map[string]any{ "deptId": deptID, }) if err != nil { return err } dept, err := strictDeptDetail(data, deptID, "contact/get_dept_info_by_dept_id") if err != nil { return err } return rt.Output(map[string]any{"department": dept}) }, }
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{ OutputRollout: output.RolloutUnifiedActive, Service: "todo", Command: "+overdue", Product: "todo", Description: "列出我已过期未完成的待办", Intent: "当你想快速看清自己有哪些待办已经过了截止时间却还没做完、方便优先处理时使用;" + "内部先拉取你当前组织下作为执行人(executor)的待办列表,再在本地按「有截止时间(dueTime) 且早于当前时刻 且尚未完成」的条件筛选," + "最后只打印这些逾期待办的标题(subject)、截止时间(dueTime) 和任务 ID(taskId)。" + "这是纯只读操作,只做列表与本地过滤,不会修改或完成任何待办;若没有逾期待办则返回空列表。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "todo", Name: "shortcut_overdue", CanonicalPath: "todo.shortcut_overdue", CLIPath: "todo +overdue", PrimaryCLIPath: "todo +overdue", }, Description: "列出我已过期未完成的待办", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"逾期未完成待办","properties":{"overdue":{"type":"array","description":"逾期未完成任务","items":{"type":"object","description":"待办条目","additionalProperties":true}}},"required":["overdue"],"additionalProperties":false}`)}, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出我已过期未完成的待办", UseWhen: []string{"当你想快速看清自己有哪些待办已经过了截止时间却还没做完、方便优先处理时使用;内部先拉取你当前组织下作为执行人(executor)的待办列表,再在本地按「有截止时间(dueTime) 且早于当前时刻 且尚未完成」的条件筛选,最后只打印这些逾期待办的标题(subject)、截止时间(dueTime) 和任务 ID(taskId)。这是纯只读操作,只做列表与本地过滤,不会修改或完成任何待办;若没有逾期待办则返回空列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws todo +overdue"}, }, }, 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 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "mail", Name: "shortcut_recent_mail", CanonicalPath: "mail.shortcut_recent_mail", CLIPath: "mail +recent-mail", PrimaryCLIPath: "mail +recent-mail", }, Description: "列出收件箱近期邮件会话并投影列表(主题/发件人/时间/threadId)", Parameters: []contract.ParamDecl{ {Name: "limit", Property: "limit"}, {Name: "email", Property: "email"}, {Name: "folder", Property: "folder"}, {Name: "cursor", Property: "cursor"}, }, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出收件箱近期邮件会话并投影列表(主题/发件人/时间/threadId)", UseWhen: []string{"当你想快速看一眼自己邮箱里近期的邮件会话(收件箱线程/conversation),只需要一份精简清单(主题、发件人、最后修改时间、会话 threadId)、而不想翻完整正文或原始字段时使用;内部先确定要看的邮箱地址——你可以用 --email 指定,不指定时自动取你绑定的第一个邮箱——再解析要看的文件夹——你可以用 --folder 指定文件夹 ID,不指定时自动定位收件箱——然后列出该文件夹下的近期会话,最后在本地把每条会话投影成 {subject, from, date, threadId} 打印出来,可配合 --format/--jq/--fields。这是纯只读操作,只做列举与本地投影,不会修改、发送或删除任何邮件;若最近没有邮件则提示「最近没有邮件」。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws mail +recent-mail", "dws mail +recent-mail --limit 30", }, }, }, 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}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,取自上一页 nextCursor", Required: false}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"limit"}, Description: "显式 --limit 必须在 1-100 之间"}}, Validate: func(rt *shortcut.RuntimeContext) error { return smartMailValidatePageSize(rt, "limit", 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 } limit := 20 if rt.Changed("limit") { limit = rt.Int("limit") } args := map[string]any{ "email": email, "folderId": folderID, "size": limit, } if rt.Changed("cursor") { args["cursor"] = rt.Str("cursor") } data, err := rt.CallMCPData("mail", "list_mailbox_threads", args) if err != nil { return err } threads, err := smartMailRows(data, "mail/list_mailbox_threads", "result.conversations", "id") if err != nil { return err } results := make([]map[string]any, 0, len(threads)) for _, t := range threads { from, err := recentMailSenders(t) if err != nil { return err } results = append(results, map[string]any{ "subject": searchMailFirstString(t, "subject", "title", "topic"), "from": from, "date": searchMailFirstAny(t, "lastModifiedDateTime", "date", "sentTime", "sentDate", "receivedDate", "createTime"), "threadId": searchMailFirstString(t, "threadId", "id", "conversationId"), }) } complete, next, err := smartMailPage(data, "mail/list_mailbox_threads", "result", rt.Str("cursor")) if err != nil { return err } return smartMailOutputPage(rt, "mails", results, complete, next) }, }
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "aitable", Name: "shortcut_record_share_links", CanonicalPath: "aitable.shortcut_record_share_links", CLIPath: "aitable +record-share-links", PrimaryCLIPath: "aitable +record-share-links", }, Description: "批量(可 >20 条)获取多维表记录分享链接:去重+分片+合并", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "批量(可 >20 条)获取多维表记录分享链接:去重+分片+合并", UseWhen: []string{"当你要一次性拿到很多条多维表记录的分享链接、数量可能超过底层工具单次 20 条上限时使用;内部先对 --record-ids 去重(保持顺序),再按每批 ≤20 条切片,逐批调用 get_record_share_url(在 aitable-helper 服务上),最后把各批返回的 {recordId, shareUrl} 合并成一个列表;某一批失败会记录错误但不影响其余批。这是只读操作,只生成/获取分享链接、不修改记录。可选 --view-id 生成带视图上下文的链接。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []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", }, }, }, 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{ OutputRollout: output.RolloutUnifiedActive, Service: "todo", Command: "+get-related-tasks", Aliases: []string{"+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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ProductID: "todo", Name: "shortcut_get_related_tasks", CanonicalPath: "todo.shortcut_get_related_tasks", CLIPath: "todo +get-related-tasks", PrimaryCLIPath: "todo +get-related-tasks", Aliases: []string{"todo +related-tasks"}}, Description: "一次性列出与我相关的全部待办(创建人/执行人/参与人并集)", Interface: &contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "Reviewed Todo composite: strict full pagination, role union, deduplication and projection are owned by the executable shortcut."}, Selection: contract.SelectionSpec{ AgentSummary: "列出与我相关的全部待办", UseWhen: []string{"需要查看我作为创建人、执行人或参与人的全部待办并集时"}, AvoidWhen: []string{"只需默认执行人视角列表时使用 todo +get-my-tasks"}, Examples: []string{"dws todo +get-related-tasks"}, }, Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"与当前用户相关的待办并集","properties":{"count":{"type":"integer","description":"去重后的任务数量"},"tasks":{"type":"array","description":"按 taskId 去重的任务","items":{"type":"object","description":"待办条目","additionalProperties":true}}},"required":["count","tasks"],"additionalProperties":false}`)}, }, 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 +get-related-tasks`, `dws todo +get-related-tasks --role-types creator,executor`, `dws todo +get-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)) } 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{ OutputRollout: output.RolloutUnifiedActive, Service: "todo", Command: "+remind", Product: "todo", Description: "给自己创建一条带可选截止时间的待办", Intent: "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;" + "内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "todo", Name: "shortcut_remind", CanonicalPath: "todo.shortcut_remind", CLIPath: "todo +remind", PrimaryCLIPath: "todo +remind", }, Description: "给自己创建一条带可选截止时间的待办", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"已验证的自用待办","properties":{"taskId":{"type":"string","description":"新待办稳定 taskId"},"subject":{"type":"string","description":"待办标题"},"verified":{"type":"boolean","description":"是否完成详情读回核验"}},"required":["taskId","subject","verified"],"additionalProperties":false}`)}, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "给自己创建一条带可选截止时间的待办", UseWhen: []string{"当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。"}, AvoidWhen: []string{ "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令", "用户需要独立提醒规则时改用 dws todo task add-reminder;不要把 --at 解释成提醒时间", }, Examples: []string{"dws todo +remind --task \"交周报\" --at 2026-03-10T18:00:00+08:00"}, }, }, 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 } params := map[string]any{ "PersonalTodoCreateVO": vo, } if rt.DryRun() { return rt.Output(map[string]any{"dryRun": true, "executed": false, "subject": rt.Str("task")}) } data, err := rt.CallMCPWriteDataStrict("todo", "create_personal_todo", params) if err != nil { return err } taskID, _, err := todoshortcut.VerifyCreatedTodo(rt, data, "todo/create_personal_todo", rt.Str("task")) if err != nil { return err } return rt.Output(map[string]any{"taskId": taskID, "subject": rt.Str("task"), "verified": true}) }, }
Remind: create a personal todo for YOURSELF with an optional due 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,或通过 --json 接受字面量、@相对文件和 stdin。" + "命令会先读取完整逐字稿并校验所有原文、空值和重复规则,再逐项写入并重新读取完整逐字稿验证效果。" + "默认首错停止;只有显式 failure-policy=continue 才继续,但任一失败仍返回非零及 applied/failed/unattempted ledger。底层不是事务且没有自动回滚,并会同时影响逐字稿和摘要。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "minutes", Name: "shortcut_replace_batch", CanonicalPath: "minutes.shortcut_replace_batch", CLIPath: "minutes +replace-batch", PrimaryCLIPath: "minutes +replace-batch", }, Description: "预检并批量执行多组听记文字替换,逐项验证且失败必定非零", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "对一条妙记(听记)批量执行多组文字替换(原文=>替换)", UseWhen: []string{"当你要在同一条听记里一次性纠正多个词(如把多个错识别的人名/术语统一替换),而底层工具一次只能替换一组时使用;内部按多个 --pair \"原文=>替换\" 逐组调用替换工具,先在本地校验去重(同一个「原文」不能出现两次,避免两条规则互相打架),再逐组应用并聚合每组的成功/失败结果,某一组失败不会中断其余组。这是写操作,会实际修改听记文字内容,请确认 taskUuid 与替换规则无误。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws minutes +replace-batch --id <taskUuid> --pair \"张三=>张三丰\"", "dws minutes +replace-batch --id <taskUuid> --pair \"Q2=>第二季度\" --pair \"PM=>产品经理\"", }, }, DryRun: &contract.DryRunSpec{ PreviewKind: contract.DryRunPreviewPlan, RemoteReads: false, }, Parameters: []contract.ParamDecl{ {Name: "pair", Description: `替换规则,格式 "原文=>替换",可重复传多组(必填);每组原文不能为空且不能重复`}, {Name: "json", Description: "JSON 字面量、@工作目录相对文件或 - 表示 stdin"}, {Name: "failure-policy", Description: "stop=首错停止;continue=显式继续,但只要有失败仍返回非零"}, }, }, Flags: []shortcut.Flag{ {Name: "id", Type: shortcut.FlagString, Desc: "听记 taskUuid(必填)", Required: true}, {Name: "pair", Type: shortcut.FlagStringSlice, Desc: `替换规则,格式 "原文=>替换",可重复传多组`}, {Name: "json", Type: shortcut.FlagString, Desc: "替换规则 JSON 字面量、@相对文件或 - 表示 stdin"}, {Name: "failure-policy", Type: shortcut.FlagString, Default: "stop", Desc: "失败策略", Enum: []string{"stop", "continue"}}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "100", Desc: "逐字稿写前/写后验证的翻页上限"}, }, Constraints: []shortcut.Constraint{ { Kind: shortcut.ConstraintCustom, Flags: []string{"pair", "json"}, Description: `--pair 与 --json 至少提供一种;规则必须使用 "原文=>替换" 或 JSON 数组;原文不能为空且不能重复`, }, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-limit"}, Description: "--page-limit 必须大于 0"}, }, 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 { if rt.Int("page-limit") <= 0 { return apperrors.NewValidation("--page-limit 必须大于 0") } if len(rt.StrSlice("pair")) > 0 { _, err := parseReplacePairs(rt.StrSlice("pair")) return err } return nil }, Execute: func(rt *shortcut.RuntimeContext) error { taskUUID := rt.Str("id") pairs, err := loadReplacePairs(rt) 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{ "operation": "minutes.replace_batch", "dry_run": true, "dryRun": true, "preview_kind": contract.DryRunPreviewPlan, "executed": false, "failurePolicy": rt.Str("failure-policy"), "taskUuid": taskUUID, "total": len(pairs), "replacements": replacements, }) } currentText, err := minutesReplaceTranscriptText(rt, taskUUID, rt.Int("page-limit")) if err != nil { return err } for _, pair := range pairs { if strings.Count(currentText, pair.orig) == 0 { return apperrors.NewValidation(fmt.Sprintf("完整逐字稿中不存在待替换原文 %q;未执行任何写入", pair.orig)) } } results := make([]map[string]any, 0, len(pairs)) applied := 0 failed := 0 unattempted := 0 for index, p := range pairs { writeData, callErr := rt.CallMCPWriteDataStrict("minutes", "replace_minutes_text", map[string]any{ "taskUuid": taskUUID, "originalText": p.orig, "replacedText": p.repl, }) if callErr == nil { callErr = minutesdata.RequireWriteAcknowledgement("replace text", writeData) } entry := map[string]any{"originalText": p.orig, "replacedText": p.repl} acknowledged := callErr == nil if acknowledged { entry["acknowledged"] = true beforeSource := strings.Count(currentText, p.orig) beforeTarget := strings.Count(currentText, p.repl) afterText, readErr := minutesReplaceTranscriptText(rt, taskUUID, rt.Int("page-limit")) if readErr != nil { callErr = fmt.Errorf("写入已确认但逐字稿读回失败: %w", readErr) } else if !replaceReadbackVerified(currentText, afterText, p, beforeSource, beforeTarget) { callErr = fmt.Errorf("写入已确认但逐字稿读回未证明替换生效") } else { currentText = afterText entry["verified"] = true } } if callErr != nil { entry["error"] = callErr.Error() entry["remoteEffectUnknown"] = acknowledged failed++ results = append(results, entry) if rt.Str("failure-policy") == "stop" { for _, pending := range pairs[index+1:] { results = append(results, map[string]any{ "originalText": pending.orig, "replacedText": pending.repl, "unattempted": true, }) unattempted++ } break } } else { entry["applied"] = true applied++ results = append(results, entry) } } payload := map[string]any{ "ok": failed == 0, "partial": applied > 0 && (failed > 0 || unattempted > 0), "taskUuid": taskUUID, "failurePolicy": rt.Str("failure-policy"), "total": len(pairs), "applied": applied, "failed": failed, "unattempted": unattempted, "results": results, } if err := rt.Output(payload); err != nil { return err } if failed > 0 { return apperrors.NewAPI( fmt.Sprintf("批量替换未完成:成功 %d、失败 %d、未执行 %d", applied, failed, unattempted), apperrors.WithOperation("minutes/replace_minutes_text"), apperrors.WithOrigin("shortcut"), apperrors.WithFailureStage("write"), apperrors.WithReason("minutes_replace_partial_failure"), apperrors.WithExecutionStarted(true), apperrors.WithRetryable(false), apperrors.WithDetails(payload), ) } return nil }, }
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. The default policy stops at the first error; explicit continue still returns non-zero when any write fails.
dws minutes +replace-batch --id <taskUuid> --pair "张三=>张三丰" --pair "Q2=>第二季度"
var Reschedule = shortcut.Shortcut{ Service: "calendar", Command: "+reschedule", Product: "calendar", Description: "改一个已有日程的时间(只动开始/结束时间,其他字段不变)", Intent: "当你想把一个已经存在的日程改到新的时间段、又不想动标题/描述/参会人等其他内容时使用;" + "内部先用 eventId 拉一次日程详情确认它真实存在,再只更新开始和结束时间。" + "如果 eventId 查不到会直接报错,不会误改别的日程。" + "会真实修改该日程的时间。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_reschedule", CanonicalPath: "calendar.shortcut_reschedule", CLIPath: "calendar +reschedule", PrimaryCLIPath: "calendar +reschedule", }, Description: "改一个已有日程的时间(只动开始/结束时间,其他字段不变)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "改一个已有日程的时间(只动开始/结束时间,其他字段不变)", UseWhen: []string{"当你想把一个已经存在的日程改到新的时间段、又不想动标题/描述/参会人等其他内容时使用;内部先用 eventId 拉一次日程详情确认它真实存在,再只更新开始和结束时间。如果 eventId 查不到会直接报错,不会误改别的日程。会真实修改该日程的时间。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws calendar +reschedule --event EVENT_ID --start \"2026-03-10T15:00:00+08:00\" --end \"2026-03-10T16:00:00+08:00\""}, }, }, 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 := calendarSmartValidateRange(start, end); err != nil { return err } preflight, err := rt.CallMCPData("calendar", "get_calendar_detail", map[string]any{ "eventId": eventID, }) if err != nil { return err } if _, err := calendarSmartRequireEvent(preflight, "calendar/get_calendar_detail", eventID); err != nil { return err } if rt.DryRun() { return rt.Output(map[string]any{ "success": true, "dryRun": true, "executed": false, "eventId": eventID, "start": start, "end": end, }) } written, err := rt.CallMCPWriteDataStrict("calendar", "update_calendar_event", map[string]any{ "eventId": eventID, "startDateTime": start, "endDateTime": end, }) if err != nil { return err } if err := calendarSmartWriteReceipt(written, "calendar/update_calendar_event"); err != nil { return err } readback, err := rt.CallMCPData("calendar", "get_calendar_detail", map[string]any{"eventId": eventID}) if err != nil { return err } event, err := calendarSmartRequireEvent(readback, "calendar/get_calendar_detail", eventID) if err != nil { return err } if err := calendarSmartVerifyEventTimes(event, start, end); err != nil { return err } return rt.Output(map[string]any{ "success": true, "eventId": eventID, "verified": true, "event": event, }) }, }
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 时使用;" + "内部完整分页搜索并优先做大小写不敏感的精确名称匹配,只有显式 --fuzzy 才允许关键词包含匹配。" + "0 个或多个候选都会以结构化错误失败并返回候选,绝不替你猜选。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "aitable", Name: "shortcut_resolve_base", CanonicalPath: "aitable.shortcut_resolve_base", CLIPath: "aitable +resolve-base", PrimaryCLIPath: "aitable +resolve-base", }, Description: "按名称搜索多维表 Base 并解析出唯一 baseId(只读)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按名称搜索多维表 Base 并解析出唯一 baseId(只读)", UseWhen: []string{"当你只知道某个多维表 Base 的名称、想把它解析成可直接用于后续工具的 baseId 时使用;内部完整分页搜索并优先做大小写不敏感的精确名称匹配,只有显式 --fuzzy 才允许关键词包含匹配。0 个或多个候选都会以结构化错误失败并返回候选,绝不替你猜选。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws aitable +resolve-base --name 项目管理"}, }, }, Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "要解析的 Base 名称", Required: true}, {Name: "fuzzy", Type: shortcut.FlagBool, Default: "false", Desc: "精确名称无结果时允许包含匹配"}, }, Tips: []string{ `dws aitable +resolve-base --name 项目管理`, }, Execute: func(rt *shortcut.RuntimeContext) error { resolution, err := aitabletarget.ResolveBaseName(rt, rt.Str("name"), rt.Bool("fuzzy")) if err != nil { return err } return rt.Output(map[string]any{ "resolved": true, "status": resolution.Status, "matchType": resolution.MatchType, "baseId": resolution.Selected.ID, "name": resolution.Selected.Name, }) }, }
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "shortcut_resolve_dept", CanonicalPath: "contact.shortcut_resolve_dept", CLIPath: "contact +resolve-dept", PrimaryCLIPath: "contact +resolve-dept", }, Description: "按名称搜索部门并解析出唯一 deptId(只读)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按名称搜索部门并解析出唯一 deptId(只读)", UseWhen: []string{"当你只知道某个部门的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 deptId 时使用;内部按 --name 关键词调用 search_dept_by_keyword 搜索部门,再在本地投影出每个候选的 deptId 和 name。如果只命中一个部门就直接返回它的 deptId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何部门,也不会列出部门成员。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws contact +resolve-dept --name 技术部"}, }, }, 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, err := strictDeptCandidates(data, "contact/search_dept_by_keyword") if err != nil { return err } candidates := make([]map[string]any, 0, len(items)) for _, d := range items { candidates = append(candidates, map[string]any{ "deptId": strconv.FormatInt(d.id, 10), "name": d.name, }) } 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{ OutputRollout: output.RolloutUnifiedActive, Service: "wiki", Command: "+resolve-space", Product: "wiki", Description: "按名称搜索知识空间并解析出唯一 spaceId(只读)", Intent: "当你只知道某个知识空间(wiki space)的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 spaceId 时使用;" + "内部按 --name 关键词调用 search_wikiSpaces 搜索知识空间,再在本地投影出每个候选的 spaceId 和 name。" + "如果只命中一个知识空间就直接返回它的 spaceId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何知识空间。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: corecmd.ContractDecl{ Description: "按名称搜索知识空间并解析出唯一 spaceId(只读)", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"知识库名称解析结果","properties":{"resolved":{"type":"boolean","description":"是否唯一解析"},"spaceId":{"type":"string","description":"唯一知识库 ID"},"name":{"type":"string","description":"唯一知识库名称"},"count":{"type":"integer","description":"候选数量"},"candidates":{"type":"array","description":"需要消歧的候选知识库","items":{"type":"object","description":"知识库候选","additionalProperties":true}}},"required":["resolved"],"additionalProperties":true}`)}, Interface: &contract.InterfaceSpec{Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceAvailable, Reason: "Reviewed Wiki resolver: the executable CLI strictly validates search results and refuses to guess when multiple spaces match."}, Selection: contract.SelectionSpec{AgentSummary: "按名称搜索知识空间并解析出唯一 spaceId(只读)", UseWhen: []string{"当你只知道某个知识空间(wiki space)的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 spaceId 时使用;内部按 --name 关键词调用 search_wikiSpaces 搜索知识空间,再在本地投影出每个候选的 spaceId 和 name。如果只命中一个知识空间就直接返回它的 spaceId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何知识空间。"}, AvoidWhen: []string{"只想浏览所有匹配项用 wiki +space-search;已知 workspaceId 时无需解析"}, Examples: []string{`dws wiki +resolve-space --name "产品文档"`}}, Identity: contract.ToolIdentitySpec{ProductID: "wiki", Name: "shortcut_resolve_space", CanonicalPath: "wiki.shortcut_resolve_space", CLIPath: "wiki +resolve-space", PrimaryCLIPath: "wiki +resolve-space"}, Parameters: []contract.ParamDecl{{Name: "name", Property: "keyword"}}, }, 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, err := resolveSpaceItemsStrict(data) if err != nil { return err } 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 时使用;" + "内部先列出全部数据表并优先做大小写不敏感的精确名称匹配,只有显式 --fuzzy 才允许包含匹配。" + "0 个或多个候选都会以结构化错误失败并返回候选,绝不替你猜选。" + "这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "aitable", Name: "shortcut_resolve_table", CanonicalPath: "aitable.shortcut_resolve_table", CLIPath: "aitable +resolve-table", PrimaryCLIPath: "aitable +resolve-table", }, Description: "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)", UseWhen: []string{"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称、想把它解析成可直接用于后续工具的 tableId 时使用;内部先列出全部数据表并优先做大小写不敏感的精确名称匹配,只有显式 --fuzzy 才允许包含匹配。0 个或多个候选都会以结构化错误失败并返回候选,绝不替你猜选。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws aitable +resolve-table --base B --name 任务"}, }, }, Flags: []shortcut.Flag{ {Name: "base", Type: shortcut.FlagString, Desc: "Base ID(要在其内解析数据表的多维表)", Required: true}, {Name: "name", Type: shortcut.FlagString, Desc: "要解析的数据表名称", Required: true}, {Name: "fuzzy", Type: shortcut.FlagBool, Default: "false", Desc: "精确名称无结果时允许包含匹配"}, }, Tips: []string{ `dws aitable +resolve-table --base B --name 任务`, }, Execute: func(rt *shortcut.RuntimeContext) error { resolution, err := aitabletarget.ResolveTableName(rt, rt.Str("base"), rt.Str("name"), rt.Bool("fuzzy")) if err != nil { return err } return rt.Output(map[string]any{ "resolved": true, "status": resolution.Status, "matchType": resolution.MatchType, "tableId": resolution.Selected.ID, "name": resolution.Selected.Name, "base": rt.Str("base"), }) }, }
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "mail", Name: "shortcut_search_mail", CanonicalPath: "mail.shortcut_search_mail", CLIPath: "mail +search-mail", PrimaryCLIPath: "mail +search-mail", }, Description: "按 KQL 关键词搜索邮件并投影列表(主题/发件人/时间/messageId)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按 KQL 关键词搜索邮件并投影列表(主题/发件人/时间/messageId)", UseWhen: []string{"当你想按关键词(KQL 表达式,如 subject:周报、from:alice、hasAttachments:true、folderId:2 等)快速搜自己的邮件、并只看一份精简清单(主题、发件人、时间、邮件 messageId)而不想翻完整正文时使用;内部先确定要搜的邮箱地址——你可以用 --email 指定,不指定时自动取你绑定的第一个邮箱——再执行邮件搜索,最后在本地把每封邮件投影成 {subject, from, date, messageId} 打印出来,可配合 --format/--jq/--fields。这是纯只读操作,只做搜索与本地投影,不会修改、发送或删除任何邮件;若没有命中则返回空列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws mail +search-mail --query \"subject:周报\"", "dws mail +search-mail --query \"from:alice AND date>2025-06-01T00:00:00Z\"", }, }, }, 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;显式提供时必须是 1-100 之间的整数)", Required: false}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,取自上一页 nextCursor", Required: false}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"query"}, Description: "不能为空"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"size"}, Description: "1-100"}, }, Validate: func(rt *shortcut.RuntimeContext) error { if err := smartMailValidateRequiredText(rt, "query"); err != nil { return err } return smartMailValidateStringPageSize(rt, "size") }, 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 } size, err := smartMailStringPageSize(rt, "size", "20") if err != nil { return err } email := rt.Str("email") if email == "" { resolved, err := searchMailFirstMailbox(rt) if err != nil { return err } email = resolved } args := map[string]any{ "email": email, "query": rt.Str("query"), "size": size, } if rt.Changed("cursor") { args["cursor"] = rt.Str("cursor") } data, err := rt.CallMCPData("mail", "search_emails", args) if err != nil { return err } messages, err := smartMailSearchRows(data, "mail/search_emails") if err != nil { return err } out := make([]map[string]any, 0, len(messages)) for _, m := range messages { from, err := searchMailFrom(m) if err != nil { return err } out = append(out, map[string]any{ "subject": searchMailFirstString(m, "subject", "title", "topic"), "from": from, "date": searchMailFirstAny(m, "date", "sentDate", "receivedDate", "sentTime", "internalDate", "createTime"), "messageId": searchMailFirstString(m, "messageId", "id", "mailId", "emailId", "internetMessageId"), }) } complete, next, err := smartMailPage(data, "mail/search_emails", "", rt.Str("cursor")) if err != nil { return err } return smartMailOutputPage(rt, "messages", out, complete, next) }, }
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: "im", Description: "按稳定 ID、内容、时间等条件搜索消息,可校验会话范围、全量翻页并批量富化", Intent: searchMsgIntent, Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_search_msg", CanonicalPath: "chat.shortcut_search_msg", CLIPath: "chat +search-msg", PrimaryCLIPath: "chat +search-msg", }, Description: "按稳定 ID、内容、时间等条件搜索消息,可校验会话范围、全量翻页并批量富化", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.", }, Selection: contract.SelectionSpec{ AgentSummary: "按稳定 ID、内容、时间等条件搜索消息,可校验会话范围、全量翻页并批量富化", UseWhen: []string{searchMsgIntent}, AvoidWhen: []string{"只想查看或导出一个指定会话的消息记录、且没有发送者、关键词、@对象或消息类型等主要筛选条件时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"}, Examples: []string{ "dws chat +search-msg --group \"项目群\" --sender \"测试用户甲\" --page-all", "dws chat +search-msg --query \"周报\" --senders <openDingTalkId> --days 3 --page-all", }, }, }, Flags: append([]shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词"}, {Name: "keyword", Type: shortcut.FlagString, Desc: "--query 的别名", Hidden: true}, {Name: "text", Type: shortcut.FlagString, Desc: "--query 的兼容别名", Hidden: true}, {Name: "text-query", Type: shortcut.FlagString, Desc: "--query 的兼容别名", Hidden: true}, {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: "groups", Type: shortcut.FlagStringSlice, Desc: "多个群名或 openConversationId;可混合输入并逐项唯一解析"}, {Name: "chat-id", Type: shortcut.FlagStringSlice, Desc: "--groups 的 lark-cli 对齐别名;只接受 openConversationId"}, {Name: "chat-query", Type: shortcut.FlagStringSlice, Desc: "显式按群名唯一解析的兼容入口(可选,可重复或逗号分隔)"}, {Name: "senders", Type: shortcut.FlagStringSlice, Desc: "多个发送者姓名、userId 或 openDingTalkId;姓名唯一解析,稳定 ID 精确路由,通讯录无法分类时按原值 userId 查询并保留身份未验证状态"}, {Name: "sender", Type: shortcut.FlagStringSlice, Desc: "单个或多个发送者姓名、userId 或 openDingTalkId;--senders 的兼容别名,保留同样的三态解析与安全降级语义"}, {Name: "sender-query", Type: shortcut.FlagStringSlice, Desc: "显式按姓名唯一解析的兼容入口(可选,可重复或逗号分隔)"}, {Name: "at-me", Type: shortcut.FlagBool, Desc: "只搜索 @我 的消息"}, {Name: "is-at-me", Type: shortcut.FlagBool, Desc: "--at-me 的 lark-cli 对齐别名"}, {Name: "at-ids", Type: shortcut.FlagStringSlice, Desc: "@对象 userId/openDingTalkId 列表"}, {Name: "message-type", Type: shortcut.FlagString, Desc: "下层消息类型过滤值(以当前 IM Schema 为准)"}, {Name: "only-robot", Type: shortcut.FlagBool, Desc: "只搜索机器人消息"}, {Name: "conversation-type", Type: shortcut.FlagString, Desc: "下层会话类型过滤值(以当前 IM Schema 为准)"}, {Name: "chat-type", Type: shortcut.FlagString, Desc: "--conversation-type 的 lark-cli 对齐别名"}, {Name: "days", Type: shortcut.FlagInt, Desc: "默认时间窗的回溯天数", Default: "7"}, {Name: "start", Type: shortcut.FlagString, Desc: "精确开始时间(RFC3339,需与 --end/--end-time 一起传)"}, {Name: "start-time", Type: shortcut.FlagString, Desc: "--start 的 lark-cli 对齐别名(RFC3339,需与 --end/--end-time 一起传)"}, {Name: "end", Type: shortcut.FlagString, Desc: "精确结束时间(RFC3339,需与 --start/--start-time 一起传)"}, {Name: "end-time", Type: shortcut.FlagString, Desc: "--end 的 lark-cli 对齐别名(RFC3339,需与 --start/--start-time 一起传)"}, {Name: "order", Type: shortcut.FlagString, Enum: []string{"asc", "desc"}, Desc: "按消息创建时间稳定排列输出 asc/desc(可选,默认 desc)"}, {Name: "sort", Type: shortcut.FlagString, Enum: []string{"asc", "desc"}, Desc: "--order 的 lark-cli 对齐别名(可选)"}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页返回数量(1-100)", Default: "100"}, {Name: "page-size", Type: shortcut.FlagInt, Desc: "--limit 的 lark-cli 对齐别名(1-100)"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传上次的 nextCursor", Default: "0"}, {Name: "page-token", Type: shortcut.FlagString, Desc: "--cursor 的 lark-cli 对齐别名"}, {Name: "page-all", Type: shortcut.FlagBool, Desc: "自动连续拉取所有游标页"}, {Name: "page-limit", Type: shortcut.FlagInt, Desc: "--page-all 或显式会话范围本地扫描的最大页数(1-40)", Default: "20"}, {Name: "no-enrich", Type: shortcut.FlagBool, Desc: "不再按消息 ID 批量查询完整详情"}, {Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出命中消息的 reaction(默认输出)"}, }, chatshortcut.MessageResourceDownloadFlags()...), Constraints: append([]shortcut.Constraint{ { Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"query", "keyword", "text", "text-query", "group", "conversation-id", "id", "groups", "chat-id", "chat-query", "senders", "sender", "sender-query", "at-me", "is-at-me", "at-ids", "message-type", "only-robot", "conversation-type", "chat-type"}, Description: "至少指定一个内容、身份、会话或消息类型过滤条件", }, { Kind: shortcut.ConstraintCustom, Flags: []string{"start", "start-time"}, Description: "需与 --end/--end-time 一起传", }, { Kind: shortcut.ConstraintCustom, Flags: []string{"end", "end-time"}, Description: "需与 --start/--start-time 一起传", }, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"start", "start-time"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"end", "end-time"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"order", "sort"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"groups", "chat-id"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"senders", "sender"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"at-me", "is-at-me"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"conversation-type", "chat-type"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"query", "keyword", "text", "text-query"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"limit", "page-size"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"cursor", "page-token"}}, }, chatshortcut.MessageResourceDownloadConstraints()...), Tips: []string{ `dws chat +search-msg --group "项目群" --sender "测试用户甲" --page-all`, `dws chat +search-msg --group <openConversationId> --query "changefree"`, `dws chat +search-msg --senders <openDingTalkId> --at-me --days 3 --page-all`, `dws chat +search-msg --group <openConversationId> --query "changefree" --jq '.messages[] | {messageId, text}'`, }, Validate: validateSearchMsgWithResources, Execute: func(rt *shortcut.RuntimeContext) error { params, resolvedFilters, err := searchMsgParams(rt) if err != nil { return err } requestedConversationIDs, _ := params["openConversationIds"].([]string) scopedSearch := len(requestedConversationIDs) > 0 if scopedSearch { if err := validateSearchConversationScope(rt, requestedConversationIDs); err != nil { return err } delete(params, "openConversationIds") } pageLimit := 1 scanAllPages := rt.Bool("page-all") || scopedSearch if scanAllPages { pageLimit = rt.Int("page-limit") } cursor := rt.StrFirst("page-token", "cursor") messages := make([]map[string]any, 0) seen := map[string]bool{} failures := make([]map[string]any, 0) pagesFetched := 0 complete := true hasMore := false nextCursor := "" paginationKnown := true for pagesFetched < pageLimit { params["cursor"] = cursor data, callErr := rt.CallMCPData("im", "search_messages", params) if callErr != nil { if pagesFetched == 0 { return callErr } failures = append(failures, map[string]any{ "stage": "search-page", "cursor": cursor, "error": callErr.Error(), }) complete = false break } pagesFetched++ pageMessages := searchMsgItems(data) if scopedSearch { var unverifiableMessageIDs []string pageMessages, unverifiableMessageIDs = chatmsg.FilterConversationScope(pageMessages, requestedConversationIDs) if len(unverifiableMessageIDs) > 0 { return searchScopeUnverifiedError(requestedConversationIDs, unverifiableMessageIDs) } } for _, message := range pageMessages { messageID := strings.TrimSpace(fmt.Sprint(searchMsgMessageID(message))) if messageID != "" && messageID != "<nil>" { if seen[messageID] { continue } seen[messageID] = true } messages = append(messages, message) } page := chatmsg.Pagination(data) hasMoreValue, hasMoreKnown := page["hasMore"].(bool) nextCursor = strings.TrimSpace(fmt.Sprint(page["nextCursor"])) if !hasMoreKnown { if nextCursor != "" && nextCursor != "<nil>" { hasMoreValue = true } else { failures = append(failures, map[string]any{ "stage": "search-pagination", "error": "下层未返回 hasMore 或 nextCursor,无法证明结果完整", }) paginationKnown = false complete = false break } } hasMore = hasMoreValue if !scanAllPages || !hasMore { complete = !hasMore break } if nextCursor == "" || nextCursor == "<nil>" || nextCursor == cursor { failures = append(failures, map[string]any{ "stage": "search-page", "error": "下层返回 hasMore=true,但缺少可继续且会前进的 nextCursor", }) complete = false break } cursor = nextCursor } if scanAllPages && hasMore && pagesFetched == pageLimit { failures = append(failures, map[string]any{ "stage": "search-page-limit", "error": fmt.Sprintf("达到 --page-limit=%d,仍有更多结果", pageLimit), }) complete = false } enrichedCount := 0 if !rt.Bool("no-enrich") && len(messages) > 0 { var enrichFailures []map[string]any messages, enrichedCount, enrichFailures = enrichSearchMessages(rt, messages) failures = append(failures, enrichFailures...) if len(enrichFailures) > 0 { complete = false } } if scopedSearch { validatedMessages, unverifiableMessageIDs := chatmsg.FilterConversationScope(messages, requestedConversationIDs) if len(unverifiableMessageIDs) > 0 { return searchScopeUnverifiedError(requestedConversationIDs, unverifiableMessageIDs) } if len(validatedMessages) != len(messages) { return searchScopeViolationError(requestedConversationIDs, messages) } messages = validatedMessages } if len(resolvedFilters.Senders) > 0 { var unverifiableMessageIDs []string messages, unverifiableMessageIDs = filterSearchSenderScope(messages, resolvedFilters.Senders) if len(unverifiableMessageIDs) > 0 { return searchSenderScopeUnverifiedError(resolvedFilters.Senders, unverifiableMessageIDs) } } unverifiedSenderInputs := searchUnverifiedSenderInputs(resolvedFilters.Senders) if len(unverifiedSenderInputs) > 0 { failures = append(failures, map[string]any{ "stage": "sender_identity_verification", "inputs": unverifiedSenderInputs, "error": "通讯录未能确认这些混合发送者参数是姓名还是 userId;已按精确 userId 执行,但不能据此作完整否定结论", }) complete = false } order := strings.ToLower(strings.TrimSpace(rt.StrFirst("order", "sort"))) if order == "" { order = "desc" } sortMessagesByCreateTimeStable(messages, order) results := make([]map[string]any, 0, len(messages)) for _, m := range messages { results = append(results, searchMsgProjectWithReactions(m, !rt.Bool("no-reactions"))) } payload := map[string]any{ "contractVersion": chatmsg.MessageListContractVersion, "count": len(results), "messages": results, "pagesFetched": pagesFetched, "enrichedCount": enrichedCount, "complete": complete, "hasMore": hasMore, "nextCursor": "", "paginationKnown": paginationKnown, "failedCount": len(failures), "failures": failures, "queryRange": searchMessageQueryRange(params, order), "timeCoverage": searchMessageTimeCoverage(rt), "conclusionGuard": searchMessageConclusionGuard(rt, complete, len(results)), } if len(resolvedFilters.Chats) > 0 || len(resolvedFilters.Senders) > 0 { payload["resolvedFilters"] = resolvedFilters } if len(resolvedFilters.Senders) > 0 { payload["senderScope"] = map[string]any{ "targetsResolved": len(unverifiedSenderInputs) == 0, "filterApplied": true, "filterMode": "server_and_client", "resultsWithinScope": true, } if len(unverifiedSenderInputs) > 0 { payload["senderScope"].(map[string]any)["status"] = "identity_unverified" payload["senderScope"].(map[string]any)["unverifiedInputs"] = unverifiedSenderInputs } } if scopedSearch { payload["scope"] = searchScopePayload(requestedConversationIDs, paginationKnown && !hasMore) } if hasMore && nextCursor != "" && nextCursor != "<nil>" { payload["nextCursor"] = nextCursor } if rt.Bool("download-resources") { chatshortcut.AttachMessageResourceDownloads( payload, chatshortcut.DownloadMessageResources(rt, messages, ""), ) } return rt.Output(payload) }, }
var SendToGroup = shortcut.Shortcut{ Service: "chat", Command: "+send-to-group", Product: "chat", Description: "按群名或 openConversationId 直接给群发消息", Intent: "当你有群名或 openConversationId、想直接往该群发送简单文本或 Markdown 时使用;稳定 ID 不进入搜索,群名则必须唯一解析,零命中或多候选都会在发送前停止。会真实发出群消息。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_send_to_group", CanonicalPath: "chat.shortcut_send_to_group", CLIPath: "chat +send-to-group", PrimaryCLIPath: "chat +send-to-group", }, Description: "按群名或 openConversationId 直接给群发消息", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按群名或 openConversationId 直接给群发消息", UseWhen: []string{"当你有群名或 openConversationId、想直接往该群发送简单文本或 Markdown 时使用;稳定 ID 不进入搜索,群名则必须唯一解析,零命中或多候选都会在发送前停止。会真实发出群消息。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +send-to-group --group 项目冲刺 --content \"今天 5 点前提交进度\""}, }, Parameters: []contract.ParamDecl{renamedRequiredParam("content", "text")}, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群名称或 openConversationId", Required: true}, {Name: "content", Type: shortcut.FlagString, Desc: "消息内容(支持 Markdown)", Required: true, Aliases: []string{"text"}}, shortcut.AIMessageTagFlag(), }, Tips: []string{`dws chat +send-to-group --group 项目冲刺 --content "今天 5 点前提交进度"`}, Execute: func(rt *shortcut.RuntimeContext) error { groupName := rt.Str("group") text := rt.StrFirst("text", "content") resolved, err := targetresolver.ResolveChatTarget(rt, groupName, "") if err != nil { return err } return chatshortcut.ExecuteResolvedUserMarkdown( rt, chatshortcut.ResolvedUserMessageTarget{ GroupID: resolved.Selected.OpenConversationID, }, text, ) }, }
SendToGroup: message a group by its name or stable openConversationId.
Steps: normalize a stable ID, or search a name and resolve it to a single openConversationId (disambiguate on multiple matches, never guess), then 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 项目冲刺 --content "今天 5 点前提交进度"
Service: "doc", Command: "+share-doc", Product: "chat", Description: "按姓名把文档链接私信发给某人(自动解析 userId)", Intent: "当你手上已经有一个文档链接、想直接私信发给某个人而不必先查 userId 时使用;" + "内部先按姓名搜通讯录解析出唯一用户,再用 openDingTalkId 把链接拼成一条 Markdown 消息发出去," + "姓名匹配到多人时会列出候选让你区分。只发链接、不读取或改动文档本身,会真实发出消息。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_share_doc", CanonicalPath: "doc.shortcut_share_doc", CLIPath: "doc +share-doc", PrimaryCLIPath: "doc +share-doc", }, Description: "按姓名把文档链接私信发给某人(自动解析 userId)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名把文档链接私信发给某人(自动解析 userId)", UseWhen: []string{"当你手上已经有一个文档链接、想直接私信发给某个人而不必先查 userId 时使用;内部先按姓名搜通讯录解析出唯一用户,再用 openDingTalkId 把链接拼成一条 Markdown 消息发出去,姓名匹配到多人时会列出候选让你区分。只发链接、不读取或改动文档本身,会真实发出消息。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws doc +share-doc --to 张三 --url https://docs.dingtalk.com/xxx --note \"帮忙过一下\""}, }, }, 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 := resolveOpenDingTalkUser(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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_suggest_time", CanonicalPath: "calendar.shortcut_suggest_time", CLIPath: "calendar +suggest-time", PrimaryCLIPath: "calendar +suggest-time", }, Description: "按姓名解析多位参与者,推荐大家都有空的可开会时间段(自动解析 userId)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名解析多位参与者,推荐大家都有空的可开会时间段(自动解析 userId)", UseWhen: []string{"当你想为几个人凑一个大家都空闲的开会时间、但只知道他们的姓名而不想逐个手动查 userId 时使用;内部会把 --with 里的姓名逐个搜通讯录解析成唯一 userId(任何一个没匹配到或匹配到多人都会明确报出来,绝不瞎猜),再基于所有参与者的忙闲,在给定时间范围内推荐若干可用时段。只读,不创建任何日程。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []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", }, }, }, 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 { if err := calendarSmartValidateRange(rt.Str("start"), rt.Str("end")); err != nil { return err } 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 } suggestions, err := calendarSmartSuggestedSlots(data) if err != nil { return err } return rt.Output(map[string]any{"count": len(suggestions), "suggestions": suggestions, "complete": true}) }, }
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "shortcut_team", CanonicalPath: "contact.shortcut_team", CLIPath: "contact +team", PrimaryCLIPath: "contact +team", }, Description: "按姓名列出某人所在部门的成员(自动解析 userId 与 deptId)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按姓名列出某人所在部门的成员(自动解析 userId 与 deptId)", UseWhen: []string{"当你只知道某位同事的姓名、想知道 TA 所在部门里都有哪些成员时使用;内部先按姓名解析出唯一 userId,再取 TA 的组织信息拿到主部门 deptId,最后打印该部门的直接成员列表(仅本部门,不递归下级部门)。只读,不做任何修改。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws contact +team --name 张三"}, }, }, 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 := strictResolveContactUser(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 } profile, err := strictUserDetail(data, user.userID, "contact/get_user_info_by_user_ids") if err != nil { return err } deptID, err := strictPrimaryDeptID(profile, "contact/get_user_info_by_user_ids") if err != nil { return err } data, err = rt.CallMCPData("contact", "get_dept_members_by_deptId", map[string]any{ "deptIds": []string{strconv.FormatInt(deptID, 10)}, }) if err != nil { return err } members, err := strictContactMembers(data, "contact/get_dept_members_by_deptId") if err != nil { return err } return rt.Output(map[string]any{"count": len(members), "members": members}) }, }
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{ OutputRollout: output.RolloutLegacyOnly, Service: "attendance", Command: "+this-month", Product: "attendance", Description: "查我本月的考勤打卡记录(打卡流水,自动解析当前用户)", Intent: "当你想快速看自己本月的打卡流水(几点上下班打卡、打卡地址/定位方式)、又不想先查自己的 userId " + "再手动填写本月的起止时间时使用;内部先取当前登录用户的 userId,再按本地时区算出本月 1 号 00:00 到下月 1 号 00:00 的时间窗," + "最后查询你本月的打卡流水记录。只读操作,不会修改任何考勤数据;本月若还没有任何打卡则返回空结果。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "attendance", Name: "shortcut_this_month", CanonicalPath: "attendance.shortcut_this_month", CLIPath: "attendance +this-month", PrimaryCLIPath: "attendance +this-month", }, Description: "查我本月的考勤打卡记录(打卡流水,自动解析当前用户)", Interface: &contract.InterfaceSpec{ Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceAvailable, Reason: "Historical executable Schema compatibility: this command remains callable, but the reviewed Shortcut catalog keeps it non-public until a known-nonempty current-month fixture closes the false-empty proof gap.", }, Selection: contract.SelectionSpec{ AgentSummary: "查我本月的考勤打卡记录(打卡流水,自动解析当前用户)", UseWhen: []string{"当你想快速看自己本月的打卡流水(几点上下班打卡、打卡地址/定位方式)、又不想先查自己的 userId 再手动填写本月的起止时间时使用;内部先取当前登录用户的 userId,再按本地时区算出本月 1 号 00:00 到下月 1 号 00:00 的时间窗,最后查询你本月的打卡流水记录。只读操作,不会修改任何考勤数据;本月若还没有任何打卡则返回空结果。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws attendance +this-month"}, }, }, 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 := strictAttendanceCurrentUserID(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 outputStrictAttendanceRecords(rt, 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: "按主消息 ID 或 thread/topic ID 分页读取话题回复,支持完整排序与有界自动翻页", Intent: "当你已经拿到某个群里一条「话题消息」的主消息 ID 或 threadId/topicId、想快速看这条话题下的回复(谁在什么时间回复了什么)," + "而不想拿到一大坨原始消息字段时使用;可直接传 --message-id 自动只读解析会话和 thread,也可传 --group 配合 --thread-id(兼容 --topic-id)," + "可选 --time 指定手工续页边界、--limit/--page-size 指定每页条数;--page-all 沿下层毫秒级 nextCursor 自动续页,--page-limit 保持有界;--order/--sort 支持 desc,asc 需与 --page-all 一起使用," + "结果用 complete、hasMore、nextPage、stopReason 和 failures 明确证明是否完整,再在本地投影出每条回复的发言人、文本和回复时间。" + "默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_thread_replies", CanonicalPath: "chat.shortcut_thread_replies", CLIPath: "chat +thread-replies", PrimaryCLIPath: "chat +thread-replies", }, Description: "按主消息 ID 或 thread/topic ID 分页读取话题回复,支持完整排序与有界自动翻页", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed thread reader adapter: it accepts a root message ID or threadId/topicId, reads lower topic replies, projects stable message fields, orders complete results, and optionally downloads reply resources safely.", }, Selection: contract.SelectionSpec{ AgentSummary: "按主消息 ID 或 thread/topic ID 分页读取话题回复,支持完整排序与有界自动翻页", UseWhen: []string{"当你已经拿到某个群里一条「话题消息」的主消息 ID 或 threadId/topicId、想快速看这条话题下的回复(谁在什么时间回复了什么)," + "而不想拿到一大坨原始消息字段时使用;可直接传 --message-id 自动只读解析会话和 thread,也可传 --group 配合 --thread-id(兼容 --topic-id)," + "可选 --time 指定手工续页边界、--limit/--page-size 指定每页条数;--page-all 沿下层毫秒级 nextCursor 自动续页,--page-limit 保持有界;--order/--sort 支持 desc,asc 需与 --page-all 一起使用," + "结果用 complete、hasMore、nextPage、stopReason 和 failures 明确证明是否完整,再在本地投影出每条回复的发言人、文本和回复时间。" + "默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。"}, AvoidWhen: []string{"要回复 Thread 或发送新回复时不要使用此读取入口;当前没有经过验证的 thread writer Shortcut"}, Examples: []string{ "dws chat +thread-replies --message-id <rootOpenMessageId> --page-all --order asc", "dws chat +thread-replies --group <openConversationId> --thread-id <threadId>", }, }, }, Flags: append([]shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群会话 ID;--thread-id/--topic-id 必须同时提供 --group;--group 与 --message-id 解析出的 conversationId 必须匹配"}, {Name: "message-id", Type: shortcut.FlagString, Desc: "话题主消息 openMessageId;自动只读解析 conversationId 和 threadId;--group 与 --message-id 解析出的 conversationId 必须匹配"}, {Name: "thread-id", Type: shortcut.FlagString, Desc: "话题/线程 ID(可直接使用消息列表返回的 threadId);--thread-id/--topic-id 必须同时提供 --group"}, {Name: "topic-id", Type: shortcut.FlagString, Desc: "--thread-id 的兼容别名;--thread-id/--topic-id 必须同时提供 --group"}, {Name: "time", Type: shortcut.FlagString, Desc: "起始时间,如 \"2025-03-01 00:00:00\";--time 必须是 RFC3339、YYYY-MM-DD HH:mm:ss 或 YYYY-MM-DD(可选)"}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页拉取的回复条数;--limit 必须大于 0"}, {Name: "page-size", Type: shortcut.FlagInt, Desc: "--limit 的公开兼容别名;必须大于 0"}, {Name: "page-all", Type: shortcut.FlagBool, Desc: "沿下层毫秒级 nextCursor 自动读取后续页;--page-limit 仅与 --page-all 一起使用且范围 1-500;asc 必须与 --page-all 一起使用"}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, {Name: "order", Type: shortcut.FlagString, Enum: []string{"asc", "desc"}, Desc: "回复输出顺序 asc/desc(可选,默认 desc;asc 必须与 --page-all 一起使用)"}, {Name: "sort", Type: shortcut.FlagString, Enum: []string{"asc", "desc"}, Desc: "--order 的 lark-cli 对齐别名(可选;asc 必须与 --page-all 一起使用)"}, {Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出回复 reaction(默认输出)"}, }, chatshortcut.MessageResourceDownloadFlags()...), Constraints: append([]shortcut.Constraint{ { Kind: shortcut.ConstraintExactlyOne, Flags: []string{"message-id", "thread-id", "topic-id"}, Description: "--message-id、--thread-id 与兼容参数 --topic-id 必须且只能指定一个", }, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"limit", "page-size"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"order", "sort"}}, {Kind: shortcut.ConstraintCustom, Flags: []string{"group", "thread-id", "topic-id"}, Description: "--thread-id/--topic-id 必须同时提供 --group"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"group", "message-id"}, Description: "--group 与 --message-id 解析出的 conversationId 必须匹配"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"time"}, Description: "--time 必须是 RFC3339、YYYY-MM-DD HH:mm:ss 或 YYYY-MM-DD"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"limit", "page-size"}, Description: "显式页大小必须大于 0"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"order", "sort", "page-all"}, Description: "asc 必须与 --page-all 一起使用"}, }, chatshortcut.MessageResourceDownloadConstraints()...), Tips: []string{ `dws chat +thread-replies --message-id <rootOpenMessageId> --page-all --order asc`, `dws chat +thread-replies --group <openconversationId> --thread-id <threadId>`, `dws chat +thread-replies --group <openconversationId> --thread-id <threadId> --time "2025-03-01 00:00:00" --limit 20`, `dws chat +thread-replies --group <openconversationId> --thread-id <threadId> --page-size 50 --page-all --page-limit 20`, }, Validate: validateThreadReplies, Execute: executeThreadReplies, }
ThreadReplies: fetch every reply under one topic ("话题") message and print a clean projected list (speaker / text / time) instead of a raw dump.
Steps:
- accept either --group plus --thread-id/--topic-id, or resolve a root --message-id to its conversation/thread context through list_messages_by_ids;
- call list_topic_replies (chat server), optionally with startTime=--time and pageSize=--limit;
- 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.
The default path only reads and reshapes topic replies; --download-resources additionally writes resource files locally.
dws chat +thread-replies --group <openconversationId> --thread-id <threadId>
var Today = shortcut.Shortcut{ Service: "calendar", Command: "+today", Product: "calendar", Description: "列出我今天的日程(自动计算今天的起止时间,无需手动填时间范围)", Intent: "当你想快速看看『我今天有哪些日程/会议安排』时使用;" + "内部用本地时区自动把时间范围算成今天 00:00 到次日 00:00,转成毫秒时间戳," + "查询主日历(primary)下今天的全部日程。只读,不会创建或修改任何日程。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_today", CanonicalPath: "calendar.shortcut_today", CLIPath: "calendar +today", PrimaryCLIPath: "calendar +today", }, Description: "列出我今天的日程(自动计算今天的起止时间,无需手动填时间范围)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出我今天的日程(自动计算今天的起止时间,无需手动填时间范围)", UseWhen: []string{"当你想快速看看『我今天有哪些日程/会议安排』时使用;内部用本地时区自动把时间范围算成今天 00:00 到次日 00:00,转成毫秒时间戳,查询主日历(primary)下今天的全部日程。只读,不会创建或修改任何日程。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws calendar +today"}, }, }, 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", } events, err := calendarSmartListAll(rt, toolArgs) if err != nil { return err } projected := calendarProjectEvents(events) return rt.Output(map[string]any{"count": len(projected), "events": projected, "complete": true}) }, }
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{ OutputRollout: output.RolloutUnifiedActive, Service: "todo", Command: "+todo-done", Product: "todo", Description: "按标题关键词把我的某条待办标记完成(自动定位 taskId)", Intent: "当你只记得某条待办的标题关键词、想直接把它标记完成,却不想先翻列表复制 taskId 时使用;" + "内部先拉取你当前组织下作为执行人的待办列表,按标题(subject)包含关键词匹配:没匹配到会提示「没找到匹配待办」," + "匹配到多条会列出候选(标题+taskId)让你写得更精确,唯一命中时才把它标记为已完成。这会真实修改待办完成状态。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "todo", Name: "shortcut_todo_done", CanonicalPath: "todo.shortcut_todo_done", CLIPath: "todo +todo-done", PrimaryCLIPath: "todo +todo-done", }, Description: "按标题关键词把我的某条待办标记完成(自动定位 taskId)", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"已验证的完成结果","properties":{"taskId":{"type":"string","description":"已完成待办 taskId"},"subject":{"type":"string","description":"已完成待办标题"},"verified":{"type":"boolean","description":"是否完成详情读回核验"}},"required":["taskId","subject","verified"],"additionalProperties":false}`)}, Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按标题关键词把我的某条待办标记完成(自动定位 taskId)", UseWhen: []string{"当你只记得某条待办的标题关键词、想直接把它标记完成,却不想先翻列表复制 taskId 时使用;内部先拉取你当前组织下作为执行人的待办列表,按标题(subject)包含关键词匹配:没匹配到会提示「没找到匹配待办」,匹配到多条会列出候选(标题+taskId)让你写得更精确,唯一命中时才把它标记为已完成。这会真实修改待办完成状态。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws todo +todo-done --task 周报"}, }, }, 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), ";"))) } if rt.DryRun() { return rt.Output(map[string]any{"dryRun": true, "executed": false, "taskId": matches[0].taskID}) } data, err := rt.CallMCPWriteDataStrict("todo", "update_todo_done_status", map[string]any{ "taskId": matches[0].taskID, "isDone": "true", }) if err != nil { return err } if err := todoshortcut.VerifyDoneStatus(rt, data, matches[0].taskID, true); err != nil { return err } return rt.Output(map[string]any{"taskId": matches[0].taskID, "subject": matches[0].subject, "verified": 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_tomorrow", CanonicalPath: "calendar.shortcut_tomorrow", CLIPath: "calendar +tomorrow", PrimaryCLIPath: "calendar +tomorrow", }, Description: "列出我明天的日程(自动计算明天的起止时间,无需手动填时间范围)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出我明天的日程(自动计算明天的起止时间,无需手动填时间范围)", UseWhen: []string{"当你想快速看看『我明天有哪些日程/会议安排』、提前准备时使用;内部用本地时区自动把时间范围算成明天 00:00 到后天 00:00,转成毫秒时间戳,查询主日历(primary)下明天的全部日程,并投影出标题、开始时间、结束时间、地点、eventId。只读,不会创建或修改任何日程。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws calendar +tomorrow"}, }, }, 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", } events, err := calendarSmartListAll(rt, toolArgs) if err != nil { return err } projected := calendarProjectEvents(events) return rt.Output(map[string]any{"count": len(projected), "events": projected, "complete": true}) }, }
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{ OutputRollout: output.RolloutUnifiedActive, Service: "minutes", Command: "+transcript", Product: "minutes", Description: "读取指定或我最新一条听记的完整逐字稿,并交付分页完整性证据", Intent: "当你要读取已知 taskUuid(--id)的完整逐字稿,或不传 --id 自动选择自己最新听记时使用;" + "默认追完 nextToken、跨页去重,并在 data 输出 complete/pages、在 meta.pagination 输出端点耗尽状态和续页 token;只有显式 --single-page 才停在一页。" + "可用 --direction 控制排序,--keyword 仅在自动选择最新听记时缩小候选;任何分页漂移或中途失败都返回非零而不是把部分原文当成全集。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: withMinutesTranscriptResult(minutesSmartContract( "+transcript", "读取指定或最新听记的完整逐字稿", "需要读取逐字稿并自动追完 nextToken、去重段落,同时看到 data.complete/data.pages 与 meta.pagination 完整性证据时使用;不传 --id 时严格选择最新听记。", []string{"只需单个原始分页响应时使用底层转写命令;需要汇总多个制品时使用 +detail"}, []string{"dws minutes +transcript --id <taskUuid>", "dws minutes +transcript --keyword 周会"}, nil, )), Flags: []shortcut.Flag{ {Name: "id", Type: shortcut.FlagString, Desc: "听记 taskUuid;不传时选择我最新的一条"}, {Name: "keyword", Type: shortcut.FlagString, Desc: "按关键字过滤听记(可选)", Required: false}, {Name: "direction", Type: shortcut.FlagString, Desc: "排序方向: 0=正序(默认), 1=倒序(可选)", Required: false, Enum: []string{"0", "1"}}, {Name: "cursor", Type: shortcut.FlagString, Desc: "单页/续拉的起始 nextToken"}, {Name: "single-page", Type: shortcut.FlagBool, Desc: "只读取一页;输出 data.complete 与 meta.pagination.next_token"}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "100", Desc: "自动翻页安全上限"}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"page-limit"}, Description: "--page-limit 必须大于 0"}}, Tips: []string{ `dws minutes +transcript --id <taskUuid>`, `dws minutes +transcript --keyword 周会`, }, Validate: func(rt *shortcut.RuntimeContext) error { if rt.Int("page-limit") <= 0 { return apperrors.NewValidation("--page-limit 必须大于 0") } return nil }, Execute: func(rt *shortcut.RuntimeContext) error { taskUUID := rt.Str("id") if taskUUID == "" { 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, err = latestMinutesTaskUUID(data) if err != nil { return err } if taskUUID == "" { return apperrors.NewValidation("暂无妙记") } } direction := rt.Str("direction") if direction == "" { direction = "0" } result, readErr := collectMinutesTranscript(rt, taskUUID, direction, rt.Str("cursor"), rt.Bool("single-page"), rt.Int("page-limit")) payload := minutesdata.TranscriptPayload(taskUUID, direction, result) return outputMinutesTranscriptResult(rt, payload, result, readErr) }, }
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 TriageMail = shortcut.Shortcut{ Service: "mail", Command: "+triage", Product: "mail", Description: "列出或筛选邮件摘要,自动解析邮箱与收件箱", Intent: "快速浏览收件箱摘要,或用 KQL 条件筛选邮件;不提供条件时自动定位当前邮箱的收件箱,返回可继续交给 +message/+messages 的稳定 ID。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Description: "列出或筛选邮件摘要,自动解析邮箱与收件箱", Identity: contract.ToolIdentitySpec{ ProductID: "mail", Name: "shortcut_triage", CanonicalPath: "mail.shortcut_triage", CLIPath: "mail +triage", PrimaryCLIPath: "mail +triage", }, Interface: &contract.InterfaceSpec{Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceAvailable, Reason: "Resolves the current mailbox and inbox locally, then strictly validates search_emails summaries and pagination."}, Selection: contract.SelectionSpec{ AgentSummary: "列出或筛选邮件摘要,自动解析邮箱与收件箱", UseWhen: []string{"快速浏览收件箱摘要,或用 KQL 条件筛选邮件;不提供条件时自动定位当前邮箱的收件箱,返回可继续交给 +message/+messages 的稳定 ID。"}, AvoidWhen: []string{"已知单个 messageId 并要读正文时用 mail +message;需要多封完整正文时用 mail +messages"}, Examples: []string{`dws mail +triage --limit 20 --format json`, `dws mail +triage --query "isRead:false" --format json`}, }, Parameters: []contract.ParamDecl{{Name: "limit", Property: "size"}, {Name: "cursor", Property: "cursor"}, {Name: "query", Property: "query"}}, }, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "可选 KQL 条件;不传时列出收件箱"}, {Name: "email", Type: shortcut.FlagString, Desc: "邮箱地址;不传时自动取当前身份首个邮箱"}, {Name: "limit", Type: shortcut.FlagInt, Default: "20", Desc: "每页摘要数量,必须在 1-100 之间"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标"}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"limit"}, Description: "1-100"}}, Validate: func(rt *shortcut.RuntimeContext) error { return smartMailValidatePageSize(rt, "limit", true) }, Execute: func(rt *shortcut.RuntimeContext) error { email := rt.Str("email") if email == "" { resolved, err := searchMailFirstMailbox(rt) if err != nil { return err } email = resolved } query := rt.Str("query") if query == "" { folderID, err := recentMailInboxFolder(rt, email) if err != nil { return err } query = "folderId:" + folderID } args := map[string]any{"email": email, "query": query, "size": strconv.Itoa(rt.Int("limit"))} if rt.Changed("cursor") { args["cursor"] = rt.Str("cursor") } data, err := rt.CallMCPData("mail", "search_emails", args) if err != nil { return err } messages, err := smartMailSearchRows(data, "mail/search_emails") if err != nil { return err } rows := make([]map[string]any, 0, len(messages)) for _, message := range messages { from, err := searchMailFrom(message) if err != nil { return err } rows = append(rows, map[string]any{ "messageId": searchMailFirstString(message, "id"), "subject": searchMailFirstString(message, "subject"), "from": from, "date": searchMailFirstAny(message, "receivedDateTime", "date", "sentDateTime"), }) } complete, next, err := smartMailPage(data, "mail/search_emails", "", rt.Str("cursor")) if err != nil { return err } return smartMailOutputPage(rt, "messages", rows, complete, next) }, }
TriageMail aligns the Lark task-level triage entry while preserving DWS's easier mailbox and inbox resolution. It returns summaries only; callers use +message or +messages for full bodies.
var UnreadChats = shortcut.Shortcut{ Service: "chat", Command: "+unread-chats", Product: "chat", Description: "列出我有未读消息的会话(投影会话名/未读数/会话ID)", Intent: "当你想快速看清自己当前有哪些会话还有未读消息、方便逐个处理时使用;" + "内部调用未读会话列表接口,可用 --count 控制返回的会话条数(不传则用服务端默认值)," + "用 --exclude-muted 排除你已设置免打扰的会话;再在本地把每个会话投影成会话名、未读数和会话 ID 三个关键字段。" + "这是纯只读操作,只做列表与本地投影,不会把任何会话标记为已读或未读;若没有未读会话则返回空列表。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_unread_chats", CanonicalPath: "chat.shortcut_unread_chats", CLIPath: "chat +unread-chats", PrimaryCLIPath: "chat +unread-chats", }, Description: "列出我有未读消息的会话(投影会话名/未读数/会话ID)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出我有未读消息的会话(投影会话名/未读数/会话ID)", UseWhen: []string{"当你想快速看清自己当前有哪些会话还有未读消息、方便逐个处理时使用;内部调用未读会话列表接口,可用 --count 控制返回的会话条数(不传则用服务端默认值),用 --exclude-muted 排除你已设置免打扰的会话;再在本地把每个会话投影成会话名、未读数和会话 ID 三个关键字段。这是纯只读操作,只做列表与本地投影,不会把任何会话标记为已读或未读;若没有未读会话则返回空列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws chat +unread-chats", "dws chat +unread-chats --count 20", }, }, }, Flags: []shortcut.Flag{ {Name: "count", Type: shortcut.FlagInt, Desc: "返回未读会话条数;显式 --count 必须大于 0,不传则使用服务端默认值", Required: false}, {Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "是否排除已设置免打扰的会话(可选,默认 false)", Required: false}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"count"}, Description: "显式 --count 必须大于 0"}, }, Tips: []string{ `dws chat +unread-chats`, `dws chat +unread-chats --count 20`, `dws chat +unread-chats --exclude-muted`, }, Validate: func(rt *shortcut.RuntimeContext) error { if rt.Changed("count") && rt.Int("count") <= 0 { return localChatOptionError("invalid_page_size", "+unread-chats 的 --count 必须大于 0", "--count") } return nil }, 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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "mail", Name: "shortcut_unread_mail", CanonicalPath: "mail.shortcut_unread_mail", CLIPath: "mail +unread-mail", PrimaryCLIPath: "mail +unread-mail", }, Description: "列出未读邮件并投影列表(主题/发件人/时间/messageId)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出未读邮件并投影列表(主题/发件人/时间/messageId)", UseWhen: []string{"当你想快速看自己邮箱里有哪些未读邮件、并只看一份精简清单(主题、发件人、时间、邮件 messageId)而不想翻完整正文时使用;内部先确定要查的邮箱地址——你可以用 --email 指定,不指定时自动取你绑定的第一个邮箱——再用 KQL 过滤条件 isRead:false 搜索未读邮件,最后在本地把每封邮件投影成 {subject, from, date, messageId} 打印出来,可配合 --format/--jq/--fields。这是纯只读操作,只做搜索与本地投影,不会把邮件标记为已读,也不会修改、发送或删除任何邮件;若没有未读邮件则返回空列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws mail +unread-mail", "dws mail +unread-mail --email user@company.com", }, }, }, Flags: []shortcut.Flag{ {Name: "email", Type: shortcut.FlagString, Desc: "要查询的邮箱地址(可选,默认取你绑定的第一个邮箱)", Required: false}, {Name: "size", Type: shortcut.FlagString, Desc: "返回条数上限(可选,默认 20;显式提供时必须是 1-100 之间的整数)", Required: false}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,取自上一页 nextCursor", Required: false}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"size"}, Description: "显式 --size 必须在 1-100 之间"}}, Validate: func(rt *shortcut.RuntimeContext) error { return smartMailValidateStringPageSize(rt, "size") }, 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 { size, err := smartMailStringPageSize(rt, "size", "20") if err != nil { return err } email := rt.Str("email") if email == "" { resolved, err := searchMailFirstMailbox(rt) if err != nil { return err } email = resolved } args := map[string]any{ "email": email, "query": "isRead:false", "size": size, } if rt.Changed("cursor") { args["cursor"] = rt.Str("cursor") } data, err := rt.CallMCPData("mail", "search_emails", args) if err != nil { return err } messages, err := smartMailSearchRows(data, "mail/search_emails") if err != nil { return err } out := make([]map[string]any, 0, len(messages)) for _, m := range messages { from, err := searchMailFrom(m) if err != nil { return err } out = append(out, map[string]any{ "subject": searchMailFirstString(m, "subject", "title", "topic"), "from": from, "date": searchMailFirstAny(m, "date", "sentDate", "receivedDate", "sentTime", "internalDate", "createTime"), "messageId": searchMailFirstString(m, "messageId", "id", "mailId", "emailId", "internetMessageId"), }) } complete, next, err := smartMailPage(data, "mail/search_emails", "", rt.Str("cursor")) if err != nil { return err } return smartMailOutputPage(rt, "messages", out, complete, next) }, }
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "shortcut_week", CanonicalPath: "calendar.shortcut_week", CLIPath: "calendar +week", PrimaryCLIPath: "calendar +week", }, Description: "列出我本周的日程(自动按周一为周首计算本周起止时间,无需手动填时间范围)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出我本周的日程(自动按周一为周首计算本周起止时间,无需手动填时间范围)", UseWhen: []string{"当你想快速看看『我本周有哪些日程/会议安排』时使用;内部用本地时区、以周一为一周开始,自动把时间范围算成本周一 00:00 到下周一 00:00,转成毫秒时间戳,查询主日历(primary)下本周的全部日程,并投影出标题、开始时间、结束时间、eventId。只读,不会创建或修改任何日程。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws calendar +week"}, }, }, 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(), } events, err := calendarSmartListAll(rt, params) if err != nil { return err } projected := calendarProjectEvents(events) return rt.Output(map[string]any{"count": len(projected), "events": projected, "complete": true}) }, }
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, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "contact", Name: "shortcut_me", CanonicalPath: "contact.shortcut_me", CLIPath: "contact +me", PrimaryCLIPath: "contact +me", }, Description: "查看我自己的通讯录资料(姓名/userId/手机/部门/组织,干净投影)", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "查看我自己的通讯录资料(姓名/userId/手机/部门/组织,干净投影)", UseWhen: []string{"当你(或 AI agent)需要知道「我是谁」——我自己的 userId、姓名、所在部门、组织、手机号,用于后续按名解析他人前先确定自己身份、或填充发起人信息时使用;内部调用零参数的 get_current_user_profile(永远是「我」,无需传姓名),再把冗长的原始资料投影成 {name,userId,mobile,dept,org,email} 几个关键字段。这是纯只读操作,不修改任何资料。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws contact +me"}, }, }, 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 } profile, err := strictWhoami(data) if err != nil { return err } return rt.Output(profile) }, }
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{ OutputRollout: output.RolloutUnifiedActive, Service: "wiki", Command: "+wiki-new-doc", Product: "wiki", Description: "在指定名称的知识库下新建一个文档节点(自动按空间名解析 workspaceId)", Intent: "当你只知道知识库(知识空间)的名字、想直接在它下面新建一篇文档,却不想先搜索空间、复制 workspaceId 再建节点时使用;" + "内部先按空间名搜索知识库,若唯一命中则拿到它的 workspaceId,再在该库根目录下创建一个在线文档节点。" + "如果这个名字没有匹配到任何知识库,或匹配到多个,会报错让你用更精确的名字,绝不乱猜。" + "这会真实创建一个新的文档节点。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent"}, Contract: corecmd.ContractDecl{ Description: "在指定名称的知识库下新建一个文档节点(自动按空间名解析 workspaceId)", Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object","description":"已验证的新建 Wiki 文档","properties":{"success":{"type":"boolean","description":"是否成功"},"nodeId":{"type":"string","description":"新文档节点 ID"},"space":{"type":"string","description":"请求的知识库名称"},"title":{"type":"string","description":"请求的文档标题"},"document":{"type":"object","description":"读回的文档元数据","additionalProperties":true}},"required":["success","nodeId","space","title","document"],"additionalProperties":true}`)}, Interface: &contract.InterfaceSpec{Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceAvailable, Reason: "Reviewed Wiki smart Shortcut: the executable CLI strictly resolves one exact space, creates a document, and verifies it through a metadata read-back."}, Selection: contract.SelectionSpec{AgentSummary: "在指定名称的知识库下新建一个文档节点(自动按空间名解析 workspaceId)", UseWhen: []string{"当你只知道知识库(知识空间)的名字、想直接在它下面新建一篇文档,却不想先搜索空间、复制 workspaceId 再建节点时使用;内部先按空间名搜索知识库,若唯一命中则拿到它的 workspaceId,再在该库根目录下创建一个在线文档节点。如果这个名字没有匹配到任何知识库,或匹配到多个,会报错让你用更精确的名字,绝不乱猜。这会真实创建一个新的文档节点。"}, AvoidWhen: []string{"已知 workspaceId 时用 wiki +node-create;空间名不唯一时先用 wiki +space-search"}, Examples: []string{`dws wiki +wiki-new-doc --space "产品文档库" --title "需求评审纪要"`}}, Identity: contract.ToolIdentitySpec{ProductID: "wiki", Name: "shortcut_wiki_new_doc", CanonicalPath: "wiki.shortcut_wiki_new_doc", CLIPath: "wiki +wiki-new-doc", PrimaryCLIPath: "wiki +wiki-new-doc"}, Parameters: []contract.ParamDecl{{Name: "space", Property: "keyword"}, {Name: "title", Property: "name"}}, }, 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.CallMCPWriteDataStrict("doc", "create_file", map[string]any{ "workspaceId": workspaceID, "name": title, "type": "adoc", }) if err != nil { return err } if success, ok := created["success"].(bool); !ok || !success { return apperrors.NewAPI("create_file 未返回 success=true,无法证明文档已创建", apperrors.WithOperation("doc/create_file"), apperrors.WithReason("missing_terminal_success")) } nodeID := wikiNewDocFirstString(created, "nodeId", "fileId", "id") if nodeID == "" { return apperrors.NewAPI("create_file 未返回 nodeId,远端效果未知", apperrors.WithOperation("doc/create_file"), apperrors.WithReason("missing_created_id")) } verified, err := rt.CallMCPData("doc", "get_document_info", map[string]any{"nodeId": nodeID}) if err != nil { return err } if success, present := verified["success"]; present { value, ok := success.(bool) if !ok || !value { return apperrors.NewAPI("新建文档读回未成功", apperrors.WithOperation("doc/get_document_info"), apperrors.WithReason("readback_failed")) } } if wikiNewDocFirstString(verified, "nodeId", "fileId", "id") != nodeID { return apperrors.NewAPI("新建文档读回 nodeId 不一致", apperrors.WithOperation("doc/get_document_info"), apperrors.WithReason("readback_id_mismatch")) } return rt.Output(map[string]any{ "success": true, "nodeId": nodeID, "space": spaceName, "title": title, "document": verified, }) }, }
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
- assign.go
- assign_multi.go
- at_me.go
- attendance_strict.go
- book.go
- broadcast.go
- by_mobile.go
- calendar_shared.go
- cancel_event.go
- chat_messages.go
- conflicts.go
- contact_strict.go
- created_todos.go
- dept_members.go
- dm.go
- doc_access.go
- doc_append.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
- local_chat_validation.go
- lookup.go
- mail_strict.go
- message_time_range.go
- minutes_contract.go
- minutes_detail.go
- minutes_search.go
- my_attendance.go
- my_free.go
- my_groups.go
- next_event.go
- org.go
- overdue.go
- primary_param_contract.go
- recent_mail.go
- record_share_links.go
- related_tasks.go
- remind.go
- replace_batch.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
- triage_mail.go
- unread_chats.go
- unread_mail.go
- week.go
- whoami.go
- wiki_new_doc.go