// Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package chat import ( "fmt" "strconv" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/targetresolver" ) const ( chatSearchHardPageLimit = 500 chatSearchMaxWindowSize = 100 chatListAllHardPageLimit = 500 ) // ChatSearch searches groups by keyword (search_groups on the im server). var ChatSearch = shortcut.Shortcut{ Service: "chat", Command: "+chat-search", Aliases: []string{"+chat-group-search", "+search-group"}, SinglePositionalAliasFor: "query", Product: "im", Description: "按关键词分页搜索群聊,支持有界自动翻页和完整性检查", Intent: "当你只记得群名称关键词、需要拿到群 openConversationId 以便发消息或管理该群时使用;默认读取一页,明确要求全部候选时加 --page-all,并用 --page-limit 保持有界。结果按 openConversationId 去重,并公开 complete、hasMore、nextCursor、stopReason 和 failures,避免把截断或失败结果误当完整候选集。", 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_search", CanonicalPath: "chat.shortcut_chat_search", CLIPath: "chat +chat-search", PrimaryCLIPath: "chat +chat-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{"当你只记得群名称关键词、需要拿到群 openConversationId 以便发消息或管理该群时使用;默认读取一页,明确要求全部候选时加 --page-all,并用 --page-limit 保持有界。结果按 openConversationId 去重,并公开 complete、hasMore、nextCursor、stopReason 和 failures,避免把截断或失败结果误当完整候选集。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-search --query \"项目冲刺\""}, }, }, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "群名称关键词"}, {Name: "keyword", Type: shortcut.FlagString, Desc: "--query 的别名", Hidden: true}, {Name: "limit", Type: shortcut.FlagInt, Default: "20", Desc: "每页返回数量;显式页大小必须在 1-100 之间"}, {Name: "page-size", Type: shortcut.FlagInt, Desc: "--limit 的 Lark 对齐别名;显式页大小必须在 1-100 之间"}, {Name: "size", Type: shortcut.FlagInt, Desc: "--limit 的旧版别名", Hidden: true}, {Name: "cursor", Type: shortcut.FlagString, Default: "0", Desc: "分页游标,翻页传 nextCursor"}, {Name: "page-token", Type: shortcut.FlagString, Desc: "--cursor 的 Lark 对齐别名"}, {Name: "page-all", Type: shortcut.FlagBool, Desc: "自动读取全部群搜索分页;--page-limit 仅与 --page-all 一起使用且范围 1-500"}, {Name: "page-limit", Type: shortcut.FlagInt, Default: "50", Desc: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, {Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "排除已设置免打扰的群聊"}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"query", "keyword"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"limit", "page-size", "size"}}, {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"cursor", "page-token"}}, {Kind: shortcut.ConstraintCustom, Flags: []string{"limit", "page-size"}, Description: "显式页大小必须在 1-100 之间"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"page-all", "page-limit"}, Description: "--page-limit 仅与 --page-all 一起使用且范围 1-500"}, }, Tips: []string{ `dws chat +chat-search --query "项目冲刺"`, `dws chat +chat-search --query "项目" --page-size 100 --page-all --page-limit 20`, }, Validate: validateChatSearch, Execute: executeChatSearch, } func validateChatSearch(rt *shortcut.RuntimeContext) error { if size := chatSearchPageSize(rt); size < 1 || size > 100 { return apperrors.NewValidation("--limit/--page-size/--size 必须在 1-100 之间") } if !rt.Bool("page-all") && rt.Changed("page-limit") { return apperrors.NewValidation("--page-limit 仅与 --page-all 一起使用") } if rt.Bool("page-all") { if limit := rt.Int("page-limit"); limit < 1 || limit > chatSearchHardPageLimit { return apperrors.NewValidation("--page-limit 必须在 1-500 之间") } } return nil } func chatSearchPageSize(rt *shortcut.RuntimeContext) int { if rt.Changed("page-size") { return rt.Int("page-size") } if rt.Changed("size") { return rt.Int("size") } return rt.Int("limit") } func chatSearchStartCursor(rt *shortcut.RuntimeContext) string { if rt.Changed("page-token") { if value := strings.TrimSpace(rt.Str("page-token")); value != "" { return value } } if value := strings.TrimSpace(rt.Str("cursor")); value != "" { return value } return "0" } func chatSearchRequestParams(query string, pageSize int, cursor string, excludeMuted bool) map[string]any { params := map[string]any{ "keyword": query, "limit": pageSize, "cursor": cursor, } if excludeMuted { params["excludeMuted"] = true } return params } func executeChatSearch(rt *shortcut.RuntimeContext) error { query := strings.TrimSpace(rt.StrFirst("query", "keyword")) pageSize := chatSearchPageSize(rt) requestPageSize := pageSize pageLimit := 1 if rt.Bool("page-all") { pageLimit = rt.Int("page-limit") } cursor := chatSearchStartCursor(rt) if rt.DryRun() { return rt.CallMCP("search_groups", chatSearchRequestParams(query, pageSize, cursor, rt.Bool("exclude-muted"))) } initialCursor := cursor seenCursors := map[string]bool{cursor: true} seenChats := map[string]bool{} chats := make([]map[string]any, 0) failures := make([]map[string]any, 0) pagesFetched := 0 paginationKnown := true complete := false hasMore := false nextCursor := "" stopReason := "source_complete" truncatedByPageLimit := false maxWindowProbeUsed := false completionEvidence := "" for pagesFetched < pageLimit { params := chatSearchRequestParams(query, requestPageSize, cursor, rt.Bool("exclude-muted")) data, err := rt.CallMCPData("im", "search_groups", params) if err != nil { if pagesFetched == 0 { return err } failures = append(failures, map[string]any{ "page": pagesFetched + 1, "stage": "read", "cursor": cursor, "error": err.Error(), }) stopReason = "read_failure" break } pagesFetched++ pageItems := chatSearchItems(data) for _, chat := range pageItems { id := strings.TrimSpace(fmt.Sprint(chat["openConversationId"])) if id != "" && id != "" && seenChats[id] { continue } if id != "" && id != "" { seenChats[id] = true } chats = append(chats, chat) } page := chatmsg.Pagination(data) pageHasMore, hasMoreKnown := page["hasMore"].(bool) nextCursor = chatSearchCursorString(page["nextCursor"]) if !hasMoreKnown { switch { case nextCursor != "": pageHasMore = true case len(pageItems) < requestPageSize: paginationKnown = false complete = true hasMore = false stopReason = "legacy_short_page" default: paginationKnown = false failures = append(failures, map[string]any{ "page": pagesFetched, "stage": "pagination", "error": "群搜索返回满页结果但缺少 hasMore/nextCursor,无法证明结果完整", }) stopReason = "pagination_error" } if complete || len(failures) > 0 { break } } hasMore = pageHasMore if !hasMore { // A full first page with hasMore=false/no cursor is the live legacy // shape that can hide additional rows. Once a prior page supplied a // valid continuation cursor, its terminal hasMore=false is trustworthy. fullPageWithoutCursor := len(pageItems) >= requestPageSize && nextCursor == "" && (pagesFetched == 1 || (maxWindowProbeUsed && pagesFetched == 2)) if fullPageWithoutCursor { paginationKnown = false if rt.Bool("page-all") && initialCursor == "0" && !maxWindowProbeUsed && requestPageSize < chatSearchMaxWindowSize && pagesFetched < pageLimit { maxWindowProbeUsed = true requestPageSize = chatSearchMaxWindowSize cursor = initialCursor stopReason = "max_window_probe" continue } if !rt.Bool("page-all") { complete = false stopReason = "single_page_full_untrusted" break } if pagesFetched >= pageLimit && requestPageSize < chatSearchMaxWindowSize { truncatedByPageLimit = true complete = false stopReason = "page_limit" break } failures = append(failures, map[string]any{ "page": pagesFetched, "stage": "pagination", "error": "群搜索在最大窗口仍返回满页且没有可用 nextCursor,无法证明结果完整", }) complete = false stopReason = "pagination_error" break } complete = true nextCursor = "" if maxWindowProbeUsed { paginationKnown = false completionEvidence = "max_window_short_page" stopReason = "max_window_short_page" } else { completionEvidence = "backend_has_more_false_short_page" stopReason = "source_complete" } break } if nextCursor == "" || seenCursors[nextCursor] { failures = append(failures, map[string]any{ "page": pagesFetched, "stage": "pagination", "error": "群搜索返回 hasMore=true,但 nextCursor 缺失或未前进", }) stopReason = "pagination_error" break } if !rt.Bool("page-all") { stopReason = "single_page" break } seenCursors[nextCursor] = true cursor = nextCursor } if !complete && hasMore && len(failures) == 0 && pagesFetched >= pageLimit { truncatedByPageLimit = rt.Bool("page-all") if truncatedByPageLimit { stopReason = "page_limit" } } payload := map[string]any{ "query": query, "count": len(chats), "chats": chats, "pagesFetched": pagesFetched, "paginationKnown": paginationKnown, "complete": complete && len(failures) == 0, "hasMore": hasMore, "nextCursor": nextCursor, "stopReason": stopReason, "truncatedByPageLimit": truncatedByPageLimit, "failedCount": len(failures), "failures": failures, "partial": len(failures) > 0 && len(chats) > 0, "requestedPageSize": pageSize, "effectivePageSize": requestPageSize, } if completionEvidence != "" { payload["completionEvidence"] = completionEvidence } if rt.Bool("exclude-muted") { payload["filter"] = map[string]any{"excludeMuted": true} } if err := rt.Output(payload); err != nil { return err } if len(failures) > 0 { return apperrors.NewAPI( fmt.Sprintf("群搜索分页未完成:成功读取 %d 页,存在 %d 个失败项", pagesFetched, len(failures)), apperrors.WithOperation("im/search_groups"), apperrors.WithReason("chat_search_incomplete"), apperrors.WithOrigin("mcp_gateway"), apperrors.WithFailureStage("pagination"), apperrors.WithExecutionStarted(true), apperrors.WithRetryable(true), apperrors.WithHint("请根据 failures 和 nextCursor 重试"), ) } return nil } func chatSearchItems(data map[string]any) []map[string]any { if data == nil { return []map[string]any{} } scopes := []map[string]any{data} for _, key := range []string{"result", "data"} { if nested, ok := data[key].(map[string]any); ok { scopes = append(scopes, nested) } } for _, scope := range scopes { for _, key := range []string{"groups", "chats", "items", "list", "result"} { if raw, ok := scope[key].([]any); ok { return projectChatSearchItems(raw) } } } return []map[string]any{} } func projectChatSearchItems(raw []any) []map[string]any { out := make([]map[string]any, 0, len(raw)) for _, item := range raw { group, ok := item.(map[string]any) if !ok { continue } projected := make(map[string]any, len(group)+1) for key, value := range group { projected[key] = value } if _, exists := projected["openConversationId"]; !exists { for _, key := range []string{"conversationId", "chatId", "id"} { if value, ok := group[key]; ok { projected["openConversationId"] = value break } } } if _, exists := projected["name"]; !exists { for _, key := range []string{"title", "conversationName"} { if value, ok := group[key]; ok { projected["name"] = value break } } } out = append(out, projected) } return out } func chatSearchCursorString(value any) string { if value == nil { return "" } text := strings.TrimSpace(fmt.Sprint(value)) if text == "" || text == "" || text == "0" { return "" } return text } // ChatMembersList lists members of a group (get_group_members, chat server). // ChatMembersGet batch-queries member detail by ids (list_group_member_by_ids, im). var ChatMembersGet = shortcut.Shortcut{ Service: "chat", Command: "+chat-members-get", Product: "im", Description: "根据成员 openDingTalkId 批量查询群成员详情", Intent: "当你已有若干成员的 openDingTalkId、需要批量获取他们在该群内的详情(群昵称、角色等)时使用;只读,需传群 openConversationId 和成员 openDingTalkId 列表。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "id", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "users", Type: shortcut.FlagStringSlice, Desc: "成员 openDingTalkId 列表", Required: true}, }, Tips: []string{`dws chat +chat-members-get --id --users odid1,odid2`}, Execute: func(rt *shortcut.RuntimeContext) error { if err := validateExplicitOpenIDs("--users", rt.StrSlice("users")); err != nil { return err } return rt.CallMCP("list_group_member_by_ids", map[string]any{ "openConversationId": rt.Str("id"), "cid": rt.Str("id"), "memberOpenDingTalkIds": rt.StrSlice("users"), }) }, } // ChatMemberAdd adds members to a group (add_group_member, chat server). // ChatMemberRemove removes members from a group (remove_group_member, chat server). // ChatUpdateName renames a group (update_group_name, chat server). // ChatTransferOwner transfers group ownership (transfer_group_owner, im). var ChatTransferOwner = shortcut.Shortcut{ Service: "chat", Command: "+chat-transfer-owner", Product: "im", Description: "转让群主", Intent: "当你要把群主身份转让给他人时使用;会实际变更群主(自己不再是群主),需传群 openConversationId 和新群主的 userId 或 openDingTalkId。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "new-owner", Type: shortcut.FlagString, Desc: "新群主 userId 或 openDingTalkId", Required: true}, }, Tips: []string{`dws chat +chat-transfer-owner --group --new-owner `}, Execute: func(rt *shortcut.RuntimeContext) error { newOwner := rt.Str("new-owner") params := map[string]any{ "openConversationId": rt.Str("group"), "cid": rt.Str("group"), } if isOpenID(newOwner) { params["newOwnerOpenDingTalkId"] = newOwner } else { params["newOwnerUid"] = newOwner } return rt.CallMCP("transfer_group_owner", params) }, } // ChatInviteURL gets the group invite url (get_group_invite_url, im). var ChatInviteURL = shortcut.Shortcut{ Service: "chat", Command: "+chat-invite-url", Product: "im", Description: "获取群邀请链接", Intent: "当你想拿到一条群邀请链接分享给别人加群时使用;--group 可传群 openConversationId 或群名,多命中会安全停止。可用 --expires-seconds 设置有效期(0 表示永久)。", 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_invite_url", CanonicalPath: "chat.shortcut_chat_invite_url", CLIPath: "chat +chat-invite-url", PrimaryCLIPath: "chat +chat-invite-url", }, 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{"当你想拿到一条群邀请链接分享给别人加群时使用;--group 可传群 openConversationId 或群名,多命中会安全停止。可用 --expires-seconds 设置有效期(0 表示永久)。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-invite-url --group "}, }, }, Flags: []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: "expires-seconds", Type: shortcut.FlagInt, Desc: "链接有效期(秒),0 表示永久"}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"group", "chat-query", "group-query"}}, }, Tips: []string{ `dws chat +chat-invite-url --group `, `dws chat +chat-invite-url --group "项目群"`, }, Execute: func(rt *shortcut.RuntimeContext) error { groupID, err := resolveStableOrNamedChat(rt) if err != nil { return err } params := map[string]any{ "openConversationId": groupID, "cid": groupID, } if rt.Changed("expires-seconds") { params["expiresSeconds"] = rt.Int("expires-seconds") } return rt.CallMCP("get_group_invite_url", params) }, } // ChatQuit quits a group (quit_group, im). var ChatQuit = shortcut.Shortcut{ Service: "chat", Command: "+chat-quit", Product: "im", Description: "退出群聊", Intent: "当你想让当前用户主动退出某个群时使用;会实际退群,需传群 openConversationId。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, }, Tips: []string{`dws chat +chat-quit --group `}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("quit_group", map[string]any{"openConversationId": rt.Str("group")}) }, } // ChatUpdateIcon updates the group icon (update_group_icon, im). var ChatUpdateIcon = shortcut.Shortcut{ Service: "chat", Command: "+chat-update-icon", Product: "im", Description: "更新群头像", Intent: "当你想更换群头像时使用;会实际更新群头像,需传群 openConversationId 和已上传头像的 mediaId(以 @ 开头)。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "icon-media-id", Type: shortcut.FlagString, Desc: "群头像 mediaId(以 @ 开头)", Required: true}, }, Tips: []string{`dws chat +chat-update-icon --group --icon-media-id `}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("update_group_icon", map[string]any{ "openConversationId": rt.Str("group"), "iconMediaId": rt.Str("icon-media-id"), }) }, } // ChatUpdateSettings updates a group setting (update_group_settings, im). var ChatUpdateSettings = shortcut.Shortcut{ Service: "chat", Command: "+chat-update-settings", Product: "im", Description: "更新群设置(settingKey + status)", Intent: "当你想调整群的某项开关设置(如是否可被搜索 searchable、是否仅管理员可@所有人 onlyAdminCanAtAll)时使用;会实际修改群设置,需传 settingKey 和 status(0关/1开)。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "setting-key", Type: shortcut.FlagString, Desc: "群设置项 key,如 searchable / onlyAdminCanAtAll", Required: true}, {Name: "status", Type: shortcut.FlagInt, Desc: "设置值:0=关闭,1=开启", Required: true}, }, Tips: []string{`dws chat +chat-update-settings --group --setting-key searchable --status 1`}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("update_group_settings", map[string]any{ "openConversationId": rt.Str("group"), "settingKey": rt.Str("setting-key"), "status": rt.Int("status"), }) }, } // ChatDismiss dismisses (destroys) a group (dismiss_group, im). var ChatDismiss = shortcut.Shortcut{ Service: "chat", Command: "+chat-dismiss", Product: "im", Description: "解散群聊(不可逆,需群主权限)", Intent: "当你要彻底解散一个群时使用;会实际销毁群聊,不可逆且需群主权限,仅需传群 openConversationId,操作前务必确认。", Risk: shortcut.RiskHighWrite, Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "shortcut_chat_dismiss", CanonicalPath: "chat.shortcut_chat_dismiss", CLIPath: "chat +chat-dismiss", PrimaryCLIPath: "chat +chat-dismiss", }, 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{"当你要彻底解散一个群时使用;会实际销毁群聊,不可逆且需群主权限,仅需传群 openConversationId,操作前务必确认。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-dismiss --group "}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, }, Tips: []string{`dws chat +chat-dismiss --group `}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("dismiss_group", map[string]any{"openConversationId": rt.Str("group")}) }, } // ChatSetHistory sets new-member history visibility (update_show_history_msg_option, im). var ChatSetHistory = shortcut.Shortcut{ Service: "chat", Command: "+chat-set-history", Product: "im", Description: "设置新成员入群可查看历史消息范围", Intent: "当你想控制新成员入群后能看到多少历史消息时使用;会实际修改群配置,需传群 openConversationId 和范围(FORBIDDEN 不可见 / RECENT_100 最近100条 / ALL 全部)。", 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_chat_set_history", CanonicalPath: "chat.shortcut_chat_set_history", CLIPath: "chat +chat-set-history", PrimaryCLIPath: "chat +chat-set-history", }, 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{"当你想控制新成员入群后能看到多少历史消息时使用;会实际修改群配置,需传群 openConversationId 和范围(FORBIDDEN 不可见 / RECENT_100 最近100条 / ALL 全部)。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-set-history --group --option RECENT_100"}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "option", Type: shortcut.FlagString, Desc: "可见范围", Required: true, Enum: []string{"FORBIDDEN", "RECENT_100", "ALL"}}, }, Tips: []string{`dws chat +chat-set-history --group --option RECENT_100`}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("update_show_history_msg_option", map[string]any{ "openConversationId": rt.Str("group"), "option": rt.Str("option"), }) }, } // ChatUpdateNick sets the caller's in-group nickname (update_group_nick, im). var ChatUpdateNick = shortcut.Shortcut{ Service: "chat", Command: "+chat-update-nick", Product: "im", Description: "设置当前用户在群内的群昵称", Intent: "当你想设置当前用户在某个群里显示的群昵称时使用;会实际更新本人在该群的昵称,需传群 openConversationId 和昵称。", 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_chat_update_nick", CanonicalPath: "chat.shortcut_chat_update_nick", CLIPath: "chat +chat-update-nick", PrimaryCLIPath: "chat +chat-update-nick", }, 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{"当你想设置当前用户在某个群里显示的群昵称时使用;会实际更新本人在该群的昵称,需传群 openConversationId 和昵称。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-update-nick --group --nick \"我的群昵称\""}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "nick", Type: shortcut.FlagString, Desc: "个人群昵称", Required: true}, }, Tips: []string{`dws chat +chat-update-nick --group --nick "我的群昵称"`}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("update_group_nick", map[string]any{ "openConversationId": rt.Str("group"), "nick": rt.Str("nick"), }) }, } // ChatUpdateAlias sets the caller's private alias for a group (update_user_group_alias, im). var ChatUpdateAlias = shortcut.Shortcut{ Service: "chat", Command: "+chat-update-alias", Product: "im", Description: "设置群备注(仅自己可见)", Intent: "当你想给某个群设置仅自己可见的备注名以便区分同名群时使用;会实际保存本人对该群的备注,需传群 openConversationId 和备注标题。", 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_chat_update_alias", CanonicalPath: "chat.shortcut_chat_update_alias", CLIPath: "chat +chat-update-alias", PrimaryCLIPath: "chat +chat-update-alias", }, 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{"当你想给某个群设置仅自己可见的备注名以便区分同名群时使用;会实际保存本人对该群的备注,需传群 openConversationId 和备注标题。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-update-alias --group --alias-title \"项目A群\""}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "alias-title", Type: shortcut.FlagString, Desc: "群备注标题", Required: true}, }, Tips: []string{`dws chat +chat-update-alias --group --alias-title "项目A群"`}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("update_user_group_alias", map[string]any{ "openConversationId": rt.Str("group"), "aliasTitle": rt.Str("alias-title"), }) }, } // ChatListMine lists groups the caller owns/administers (list_owned_or_admin_groups, im). var ChatListMine = shortcut.Shortcut{ Service: "chat", Command: "+chat-list-mine", Product: "im", Description: "拉取我创建/管理的群", Intent: "当你想查看自己作为群主或管理员在管理哪些群时使用;只读分页返回,可用 --role OWNER/ADMIN 按角色过滤。", 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_list_mine", CanonicalPath: "chat.shortcut_chat_list_mine", CLIPath: "chat +chat-list-mine", PrimaryCLIPath: "chat +chat-list-mine", }, 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{"当你想查看自己作为群主或管理员在管理哪些群时使用;只读分页返回,可用 --role OWNER/ADMIN 按角色过滤。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-list-mine --role OWNER"}, }, }, Flags: []shortcut.Flag{ {Name: "role", Type: shortcut.FlagString, Desc: "角色过滤", Enum: []string{"OWNER", "ADMIN"}}, {Name: "limit", Type: shortcut.FlagInt, Desc: "最多返回群数量,不传返回全部"}, {Name: "exclude-muted", Type: shortcut.FlagBool, Desc: "排除已设置免打扰的群聊"}, }, Tips: []string{`dws chat +chat-list-mine --role OWNER`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{} if rt.Changed("role") { params["roleFilter"] = rt.Str("role") } if rt.Int("limit") > 0 { params["limit"] = rt.Int("limit") } if rt.Bool("exclude-muted") { params["excludeMuted"] = true } data, err := rt.CallMCPData("im", "list_owned_or_admin_groups", params) if err != nil { return err } groups := chatListMineProject(data) payload := map[string]any{"count": len(groups), "groups": groups} chatmsg.ApplyPagination(payload, data) return rt.Output(payload) }, } // chatListMineProject reshapes list_owned_or_admin_groups into a clean group // list ({openConversationId, name, role, ownerUserId}) — output-projection // clean output projection. List container and per-item field names are probed // defensively across candidate keys so shape drift yields an empty list rather // than a crash or fabricated data. func chatListMineProject(data map[string]any) []map[string]any { raw := chatGroupResolveList(data) out := make([]map[string]any, 0, len(raw)) for _, item := range raw { m, ok := item.(map[string]any) if !ok { continue } row := map[string]any{} if v, ok := chatGroupFirst(m, "openConversationId", "openconversation_id", "conversationId", "id"); ok { row["openConversationId"] = v } if v, ok := chatGroupFirst(m, "name", "groupName", "title"); ok { row["name"] = v } if v, ok := chatGroupFirst(m, "role", "roleType", "memberRole"); ok { row["role"] = v } if v, ok := chatGroupFirst(m, "ownerUserId", "ownerId", "owner"); ok { row["ownerUserId"] = v } if len(row) > 0 { out = append(out, row) } } return out } // ChatListAll paginates all groups the caller joined (list_my_groups_pagination, im). var ChatListAll = shortcut.Shortcut{ Service: "chat", Command: "+chat-list-all", Product: "im", Description: "分页拉取我加入的所有群列表", Intent: "当你想遍历当前用户加入的所有群做统计或批量操作时使用;只读分页返回全部已加入的群列表。", 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_list_all", CanonicalPath: "chat.shortcut_chat_list_all", CLIPath: "chat +chat-list-all", PrimaryCLIPath: "chat +chat-list-all", }, 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{"当你想遍历当前用户加入的所有群做统计或批量操作时使用;只读分页返回全部已加入的群列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-list-all --limit 50"}, }, }, Flags: append([]shortcut.Flag{ {Name: "limit", Type: shortcut.FlagInt, Default: "100", Desc: "每页返回数量;--limit 必须在 1-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 +chat-list-all --limit 50`, `dws chat +chat-list-all --limit 200 --page-all --page-limit 50`, }, Validate: validateChatListAll, Execute: executeChatListAll, } func validateChatListAll(rt *shortcut.RuntimeContext) error { if limit := rt.Int("limit"); limit < 1 || limit > 200 { return apperrors.NewValidation("--limit 必须在 1-200 之间") } if !rt.Bool("page-all") && rt.Changed("page-limit") { return apperrors.NewValidation("--page-limit 仅与 --page-all 一起使用") } if rt.Bool("page-all") { if limit := rt.Int("page-limit"); limit < 1 || limit > chatListAllHardPageLimit { return apperrors.NewValidation("--page-limit 必须在 1-500 之间") } } if err := shortcut.ValidateAutoPageControls(rt); err != nil { return apperrors.NewValidation(err.Error()) } return nil } func executeChatListAll(rt *shortcut.RuntimeContext) error { params := map[string]any{} if rt.Int("limit") > 0 { params["limit"] = rt.Int("limit") } if c := rt.Str("cursor"); c != "" && c != "0" { params["cursor"] = c } if rt.Bool("page-all") { payload, err := readAllChatListAll(rt, params) if outputErr := rt.Output(payload); outputErr != nil { return outputErr } return err } data, err := rt.CallMCPData("im", "list_my_groups_pagination", params) if err != nil { return err } groups := chatListAllProject(data) payload := map[string]any{"count": len(groups), "groups": groups} chatmsg.ApplyPagination(payload, data) payload["pagesFetched"] = 1 if payload["complete"] == true { payload["stopReason"] = "source_complete" } else { payload["stopReason"] = "single_page" } return rt.Output(payload) } func readAllChatListAll(rt *shortcut.RuntimeContext, baseParams map[string]any) (map[string]any, error) { pageLimit := rt.Int("page-limit") cursorValue := baseParams["cursor"] cursorKey := chatListAllCursorString(cursorValue) if cursorKey == "" { cursorKey = "0" } seenCursors := map[string]bool{cursorKey: true} seenGroups := map[string]bool{} allGroups := make([]map[string]any, 0) failures := make([]map[string]any, 0) pagesFetched := 0 complete := false hasMore := false stopReason := "source_complete" truncatedByPageLimit := false truncatedByResultLimit := false var nextCursor any for pagesFetched < pageLimit { if pagesFetched > 0 { if err := shortcut.WaitAutoPageDelay(rt); err != nil { failures = append(failures, map[string]any{ "page": pagesFetched + 1, "stage": "delay", "cursor": cursorKey, "error": err.Error(), }) stopReason = "delay_interrupted" break } } pageSize, _ := baseParams["limit"].(int) params := map[string]any{"limit": shortcut.AutoPageRequestSize(rt, pageSize, len(allGroups))} if cursorKey != "0" { params["cursor"] = cursorValue } data, err := rt.CallMCPData("im", "list_my_groups_pagination", params) if err != nil { if pagesFetched == 0 { return nil, err } failures = append(failures, map[string]any{ "page": pagesFetched + 1, "stage": "read", "cursor": cursorKey, "error": err.Error(), }) stopReason = "read_failure" break } pagesFetched++ pageGroups := chatListAllProject(data) overflowOnPage := false for _, group := range pageGroups { id := strings.TrimSpace(fmt.Sprint(group["openConversationId"])) if id == "" { id = "" } if id != "" && seenGroups[id] { continue } if id != "" { seenGroups[id] = true } if maxItems := rt.Int("max-items"); maxItems > 0 && len(allGroups) >= maxItems { truncatedByResultLimit = true overflowOnPage = true continue } allGroups = append(allGroups, group) } page := chatmsg.Pagination(data) pageHasMore, known := page["hasMore"].(bool) if !known { failures = append(failures, map[string]any{ "page": pagesFetched, "stage": "pagination", "error": "已加入群列表下层未返回可靠的 hasMore,无法证明结果完整", }) stopReason = "pagination_error" break } hasMore = pageHasMore if overflowOnPage { hasMore = true nextCursor = nil failures = append(failures, map[string]any{ "page": pagesFetched, "stage": "pagination", "error": "已加入群列表下层返回条数超过请求的剩余额度,无法生成不跳项的安全续页游标", }) stopReason = "pagination_error" break } if !hasMore { complete = true nextCursor = nil stopReason = "source_complete" break } nextCursor = page["nextCursor"] nextKey := chatListAllCursorString(nextCursor) if nextKey == "" || seenCursors[nextKey] { failures = append(failures, map[string]any{ "page": pagesFetched, "stage": "pagination", "error": "已加入群列表下层返回 hasMore=true,但 nextCursor 缺失、无效或未前进", }) stopReason = "pagination_error" break } seenCursors[nextKey] = true cursorKey = nextKey cursorValue = nextCursor if maxItems := rt.Int("max-items"); maxItems > 0 && len(allGroups) >= maxItems { truncatedByResultLimit = true stopReason = "result_limit" break } } if !complete && hasMore && len(failures) == 0 && pagesFetched >= pageLimit && !truncatedByResultLimit { truncatedByPageLimit = true stopReason = "page_limit" } payload := map[string]any{ "count": len(allGroups), "groups": allGroups, "pagesFetched": pagesFetched, "paginationKnown": true, "complete": complete && len(failures) == 0, "hasMore": hasMore, "stopReason": stopReason, "truncatedByPageLimit": truncatedByPageLimit, "truncatedByResultLimit": truncatedByResultLimit, "failedCount": len(failures), "failures": failures, "partial": len(failures) > 0 && len(allGroups) > 0, } chatmsg.ApplyTruncation(payload) if hasMore && nextCursor != nil { payload["nextCursor"] = nextCursor } if len(failures) == 0 { return payload, nil } return payload, apperrors.NewAPI( fmt.Sprintf("已加入群列表分页未完成:成功读取 %d 页,存在 %d 个失败项", pagesFetched, len(failures)), apperrors.WithOperation("im/list_my_groups_pagination"), apperrors.WithReason("chat_list_all_incomplete"), apperrors.WithOrigin("mcp_gateway"), apperrors.WithFailureStage("pagination"), apperrors.WithExecutionStarted(true), apperrors.WithRetryable(true), apperrors.WithHint("请根据 failures 和 nextCursor 重试"), ) } func chatListAllCursorString(value any) string { if value == nil { return "" } text := strings.TrimSpace(fmt.Sprint(value)) if text == "" || text == "" || text == "0" { return "" } return text } // chatListAllProject reshapes list_my_groups_pagination into a clean group list // ({openConversationId, name}) — clean output projection. List // container and per-item field names are probed defensively across candidate // keys so shape drift yields an empty list rather than a crash or fabricated // data. func chatListAllProject(data map[string]any) []map[string]any { raw := chatGroupResolveList(data) out := make([]map[string]any, 0, len(raw)) for _, item := range raw { m, ok := item.(map[string]any) if !ok { continue } row := map[string]any{} if v, ok := chatGroupFirst(m, "openConversationId", "openconversation_id", "conversationId", "id"); ok { row["openConversationId"] = v } if v, ok := chatGroupFirst(m, "name", "groupName", "title"); ok { row["name"] = v } if len(row) > 0 { out = append(out, row) } } return out } // chatGroupResolveList locates the list payload inside a group-list response, // tolerating a bare top-level array or nesting under common envelope keys. func chatGroupResolveList(data map[string]any) []any { if data == nil { return []any{} } // "bots" and "roles" are probed too: chatBotsProject and // chatRoleListProject reuse this resolver, while their backing tools nest // lists under result.bots and result.roles. Without those keys the // shortcuts silently return empty despite the group having bots or roles. // Group listings key on groups/conversations, which are probed first, so // adding these sibling containers is harmless to them. for _, key := range []string{"result", "data", "list", "items", "groups", "conversations", "bots", "roles"} { v, ok := data[key] if !ok { continue } if arr, ok := v.([]any); ok { return arr } if inner, ok := v.(map[string]any); ok { for _, ik := range []string{"list", "items", "groups", "conversations", "bots", "roles", "result", "data"} { if arr, ok := inner[ik].([]any); ok { return arr } } } } return []any{} } // chatGroupFirst returns the first present candidate key's value. func chatGroupFirst(m map[string]any, keys ...string) (any, bool) { for _, k := range keys { if v, ok := m[k]; ok { return v, true } } return nil, false } // ChatListJoinRequests paginates join-validation records (list_apply_join_group_records, im). var ChatListJoinRequests = shortcut.Shortcut{ Service: "chat", Command: "+chat-list-join-requests", Product: "im", Description: "分页拉取入群验证记录", Intent: "当你作为群主/管理员想查看待处理的入群申请时使用;只读分页返回入群验证记录(含 recordId、申请人与邀请人 ID),供后续用 chat-audit-join 审批。", 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_list_join_requests", CanonicalPath: "chat.shortcut_chat_list_join_requests", CLIPath: "chat +chat-list-join-requests", PrimaryCLIPath: "chat +chat-list-join-requests", }, 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{"当你作为群主/管理员想查看待处理的入群申请时使用;只读分页返回入群验证记录(含 recordId、申请人与邀请人 ID),供后续用 chat-audit-join 审批。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-list-join-requests --limit 30"}, }, }, Flags: []shortcut.Flag{ {Name: "limit", Type: shortcut.FlagInt, Default: "20", Desc: "单页数量(最大 50)"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传 nextCursor"}, }, Tips: []string{`dws chat +chat-list-join-requests --limit 30`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{} if rt.Int("limit") > 0 { params["limit"] = rt.Int("limit") } if rt.Changed("cursor") { params["cursor"] = rt.Str("cursor") } return rt.CallMCP("list_apply_join_group_records", params) }, } // ChatAuditJoin audits a join-validation record (audit_join_group, im). var ChatAuditJoin = shortcut.Shortcut{ Service: "chat", Command: "+chat-audit-join", Product: "im", Description: "审批入群验证(通过/拒绝/删除/忽略/拉黑)", Intent: "当你要处理某条入群申请时使用;会实际执行通过/拒绝/删除/忽略/拉黑动作,需传群 openConversationId、recordId、申请人与邀请人 userId 及 status。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "record-id", Type: shortcut.FlagInt, Desc: "申请记录 ID", Required: true}, {Name: "applicant", Type: shortcut.FlagString, Desc: "申请人 userId", Required: true}, {Name: "inviter", Type: shortcut.FlagString, Desc: "邀请人 userId", Required: true}, {Name: "status", Type: shortcut.FlagString, Desc: "审批动作", Required: true, Enum: []string{"AuditApprove", "AuditDelete", "AuditIgnore", "AuditRefuse", "AuditBlock"}}, {Name: "description", Type: shortcut.FlagString, Desc: "审批说明"}, }, Tips: []string{`dws chat +chat-audit-join --group --record-id 123 --applicant --inviter --status AuditApprove`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{ "openConversationId": rt.Str("group"), "applyRecordId": rt.Int("record-id"), "applicantUid": rt.Str("applicant"), "inviterUid": rt.Str("inviter"), "status": rt.Str("status"), } if rt.Changed("description") { params["auditDescription"] = rt.Str("description") } return rt.CallMCP("audit_join_group", params) }, } // ChatGetByID looks up a group by numeric group id (get_conv_info_by_group_id, im). var ChatGetByID = shortcut.Shortcut{ Service: "chat", Command: "+chat-get-by-id", Product: "im", Description: "根据群号获取群聊信息", Intent: "当你只知道群号(数字)、需要换取群 openConversationId 及群信息时使用;只读,需传 --group-id。", Risk: shortcut.RiskRead, Flags: []shortcut.Flag{ {Name: "group-id", Type: shortcut.FlagInt, Desc: "群号(数字类型)", Required: true}, }, Tips: []string{`dws chat +chat-get-by-id --group-id 12345678`}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("get_conv_info_by_group_id", map[string]any{"groupId": rt.Int("group-id")}) }, } // ChatAddBot adds a custom robot to a group (add_robot_to_group, bot). var ChatAddBot = shortcut.Shortcut{ Service: "chat", Command: "+chat-add-bot", Product: "bot", Description: "将机器人添加到群中", Intent: "当你想把某个机器人添加进群(比如让日报机器人进群播报)时使用;会实际把机器人加入群聊,需传机器人 robotCode 和群 openConversationId。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code", Required: true}, {Name: "id", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, }, Tips: []string{`dws chat +chat-add-bot --robot-code --id `}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("add_robot_to_group", map[string]any{ "robotCode": rt.Str("robot-code"), "openConversationId": rt.Str("id"), }) }, } // ChatBots lists robots in a group (list_group_bots, bot). var ChatBots = shortcut.Shortcut{ Service: "chat", Command: "+chat-bots", Product: "bot", Description: "查看群内所有机器人", Intent: "当你想查看某个群里已添加了哪些机器人时使用;--group 可传群 openConversationId 或群名,多命中会安全停止。只读返回机器人列表(含 openBotId,供后续移除)。", 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_bots", CanonicalPath: "chat.shortcut_chat_bots", CLIPath: "chat +chat-bots", PrimaryCLIPath: "chat +chat-bots", }, 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{"当你想查看某个群里已添加了哪些机器人时使用;--group 可传群 openConversationId 或群名,多命中会安全停止。只读返回机器人列表(含 openBotId,供后续移除)。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-bots --group "}, }, }, Flags: []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}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"group", "chat-query", "group-query"}}, }, Tips: []string{ `dws chat +chat-bots --group `, `dws chat +chat-bots --group "项目群"`, }, Execute: func(rt *shortcut.RuntimeContext) error { groupID, err := resolveStableOrNamedChat(rt) if err != nil { return err } data, err := rt.CallMCPData("bot", "list_group_bots", map[string]any{"openConversationId": groupID}) if err != nil { return err } bots := chatBotsProject(data) return rt.Output(map[string]any{"count": len(bots), "bots": bots}) }, } // resolveStableOrNamedChat gives read-only group shortcuts one safe target // contract. Stable cid values bypass search; natural names always go through // the shared exact-match, full-pagination and ambiguity rules. func resolveStableOrNamedChat(rt *shortcut.RuntimeContext) (string, error) { resolved, err := targetresolver.ResolveChatTarget( rt, strings.TrimSpace(rt.Str("group")), strings.TrimSpace(rt.StrFirst("chat-query", "group-query")), ) if err != nil { return "", err } return resolved.Selected.OpenConversationID, nil } // chatBotsProject reshapes list_group_bots into a clean bot list // ({openBotId, name}) — clean output projection. List container and // per-item field names are probed defensively across candidate keys so shape // drift yields an empty list rather than a crash or fabricated data. func chatBotsProject(data map[string]any) []map[string]any { raw := chatGroupResolveList(data) out := make([]map[string]any, 0, len(raw)) for _, item := range raw { m, ok := item.(map[string]any) if !ok { continue } row := map[string]any{} if v, ok := chatGroupFirst(m, "openBotId", "open_bot_id", "botId", "robotCode", "id"); ok { row["openBotId"] = v } if v, ok := chatGroupFirst(m, "name", "botName", "nick", "title"); ok { row["name"] = v } if len(row) > 0 { out = append(out, row) } } return out } // ChatRemoveBot removes a robot from a group (remove_robot_in_group, bot). var ChatRemoveBot = shortcut.Shortcut{ Service: "chat", Command: "+chat-remove-bot", Product: "bot", Description: "从群内移除机器人", Intent: "当你想把某个机器人从群里移除时使用;会实际移除机器人,不可逆,需传群 openConversationId 和机器人 openBotId。", Risk: shortcut.RiskHighWrite, Flags: []shortcut.Flag{ {Name: "id", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "bot-id", Type: shortcut.FlagString, Desc: "机器人 openBotId", Required: true}, }, Tips: []string{`dws chat +chat-remove-bot --id --bot-id `}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("remove_robot_in_group", map[string]any{ "openConversationId": rt.Str("id"), "openBotId": rt.Str("bot-id"), }) }, } // ChatSetAdmin sets/unsets group admins (update_conv_member_roles, im). var ChatSetAdmin = shortcut.Shortcut{ Service: "chat", Command: "+chat-set-admin", Product: "im", Description: "设置 / 取消群管理员", Intent: "当你想把某些成员设为或取消群管理员时使用;会实际变更成员角色,需传群 openConversationId 和成员 userId/openDingTalkId 列表,加 --off 取消管理员。", 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_chat_set_admin", CanonicalPath: "chat.shortcut_chat_set_admin", CLIPath: "chat +chat-set-admin", PrimaryCLIPath: "chat +chat-set-admin", }, 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{"当你想把某些成员设为或取消群管理员时使用;会实际变更成员角色,需传群 openConversationId 和成员 userId/openDingTalkId 列表,加 --off 取消管理员。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-set-admin --group --users userId1,userId2"}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "users", Type: shortcut.FlagStringSlice, Desc: "成员 userId 或 openDingTalkId 列表", Required: true}, {Name: "off", Type: shortcut.FlagBool, Desc: "取消管理员(不传则设为管理员)"}, }, Tips: []string{`dws chat +chat-set-admin --group --users userId1,userId2`}, Execute: func(rt *shortcut.RuntimeContext) error { userIDs, openIDs := splitIDs(rt.StrSlice("users")) params := map[string]any{ "openConversationId": rt.Str("group"), "admin": !rt.Bool("off"), } if len(userIDs) > 0 { params["uids"] = userIDs } if len(openIDs) > 0 { params["openDingTalkIds"] = openIDs } return rt.CallMCP("update_conv_member_roles", params) }, } // ChatMute mutes/unmutes the whole group (set_group_mute, im). var ChatMute = shortcut.Shortcut{ Service: "chat", Command: "+chat-mute", Product: "im", Description: "全员禁言 / 取消全员禁言", Intent: "当你想对整个群开启或取消全员禁言时使用;会实际切换群的全员禁言状态,需传群 openConversationId,加 --off 取消禁言。", 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_chat_mute", CanonicalPath: "chat.shortcut_chat_mute", CLIPath: "chat +chat-mute", PrimaryCLIPath: "chat +chat-mute", }, 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{"当你想对整个群开启或取消全员禁言时使用;会实际切换群的全员禁言状态,需传群 openConversationId,加 --off 取消禁言。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-mute --group "}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "off", Type: shortcut.FlagBool, Desc: "取消全员禁言(不传则开启禁言)"}, }, Tips: []string{`dws chat +chat-mute --group `}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("set_group_mute", map[string]any{ "openConversationId": rt.Str("group"), "mute": !rt.Bool("off"), }) }, } // ChatMuteMember mutes/unmutes specific members (set_group_member_mute_list, im). var ChatMuteMember = shortcut.Shortcut{ Service: "chat", Command: "+chat-mute-member", Product: "im", Description: "指定群成员禁言 / 取消禁言", Intent: "当你想只禁言或解禁群里的指定成员时使用;会实际把成员加入或移出禁言名单,需传群 openConversationId 和成员列表,禁言时还需 --mute-time(毫秒)。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "users", Type: shortcut.FlagStringSlice, Desc: "成员 userId 或 openDingTalkId 列表", Required: true}, {Name: "mute-time", Type: shortcut.FlagInt, Desc: "禁言时长(毫秒),如 300000/3600000/86400000/604800000/2592000000"}, {Name: "off", Type: shortcut.FlagBool, Desc: "移出禁言名单(不传则加入禁言名单)"}, }, Tips: []string{`dws chat +chat-mute-member --group --users userId1 --mute-time 3600000`}, Execute: func(rt *shortcut.RuntimeContext) error { userIDs, openIDs := splitIDs(rt.StrSlice("users")) off := rt.Bool("off") if len(userIDs) > 0 { resolved, err := resolveMuteMemberOpenIDs(rt, rt.Str("group"), userIDs) if err != nil { return err } openIDs = append(openIDs, resolved...) userIDs = nil } deduplicatedOpenIDs := make([]string, 0, len(openIDs)) for _, openID := range openIDs { deduplicatedOpenIDs = appendUniqueShortcutString(deduplicatedOpenIDs, openID) } openIDs = deduplicatedOpenIDs params := map[string]any{ "openConversationId": rt.Str("group"), "cid": rt.Str("group"), "mute": !off, } if len(userIDs) > 0 { params["uids"] = userIDs } if len(openIDs) > 0 { params["openDingTalkIds"] = openIDs } if !off { if rt.Int("mute-time") <= 0 { return fmt.Errorf("--mute-time 为禁言时必填(毫秒)") } params["muteTime"] = rt.Int("mute-time") } return rt.CallMCP("set_group_member_mute_list", params) }, } // resolveMuteMemberOpenIDs mirrors the native group-mute-member compatibility // path. The service currently rejects its documented uids input with // "uids is required", while openDingTalkIds succeeds. Resolve userIds through // the directory and the target group's member list so the Shortcut's advertised // mixed-ID contract remains executable instead of forwarding a known-broken // parameter shape. func resolveMuteMemberOpenIDs(rt *shortcut.RuntimeContext, groupID string, userIDs []string) ([]string, error) { contacts, err := rt.CallMCPData("contact", "get_user_info_by_user_ids", map[string]any{ "user_id_list": userIDs, }) if err != nil { return nil, fmt.Errorf("解析禁言成员 userId 失败: %w", err) } requested := make(map[string]struct{}, len(userIDs)) for _, userID := range userIDs { requested[userID] = struct{}{} } nameByUserID := make(map[string]string, len(userIDs)) for _, item := range shortcutMapSlice(contacts["result"]) { employee, _ := item["orgEmployeeModel"].(map[string]any) userID := shortcutString(employee, "orgUserId", "userId") if _, ok := requested[userID]; !ok { continue } nameByUserID[userID] = shortcutString(employee, "orgUserName", "name") } for _, userID := range userIDs { if strings.TrimSpace(nameByUserID[userID]) == "" { return nil, fmt.Errorf("无法从通讯录解析成员 userId %q 的姓名;请改传 openDingTalkId", userID) } } openIDsByName := map[string][]string{} cursor := "0" for page := 0; page < 50; page++ { members, callErr := rt.CallMCPData("chat", "get_group_members", map[string]any{ "openconversation_id": groupID, "cursor": cursor, }) if callErr != nil { return nil, fmt.Errorf("读取目标群成员以解析 userId 失败: %w", callErr) } result, _ := members["result"].(map[string]any) for _, member := range shortcutMapSlice(result["list"]) { name := shortcutString(member, "memberEmpName", "empName", "name") openID := shortcutString(member, "openDingtalkId", "openDingTalkId", "memberDingtalkId") if name == "" || openID == "" { continue } openIDsByName[name] = appendUniqueShortcutString(openIDsByName[name], openID) } hasMore, _ := result["hasMore"].(bool) if !hasMore { break } next := shortcutString(result, "nextCursor", "cursor") if next == "" || next == cursor { return nil, fmt.Errorf("群成员分页返回 hasMore=true 但缺少可继续的 cursor") } cursor = next if page == 49 { return nil, fmt.Errorf("群成员分页超过 50 页,无法安全解析禁言目标") } } resolved := make([]string, 0, len(userIDs)) for _, userID := range userIDs { name := nameByUserID[userID] matches := openIDsByName[name] if len(matches) != 1 { return nil, fmt.Errorf( "群内姓名 %q 对应 %d 个成员,无法把 userId %q 唯一解析为 openDingTalkId;请直接传 openDingTalkId", name, len(matches), userID) } resolved = append(resolved, matches[0]) } return resolved, nil } func shortcutMapSlice(value any) []map[string]any { items, _ := value.([]any) out := make([]map[string]any, 0, len(items)) for _, item := range items { if mapped, ok := item.(map[string]any); ok { out = append(out, mapped) } } return out } func shortcutString(value map[string]any, keys ...string) string { for _, key := range keys { switch typed := value[key].(type) { case string: if text := strings.TrimSpace(typed); text != "" { return text } case float64: return strconv.FormatFloat(typed, 'f', -1, 64) case int: return strconv.Itoa(typed) case int64: return strconv.FormatInt(typed, 10) } } return "" } func appendUniqueShortcutString(values []string, candidate string) []string { for _, value := range values { if value == candidate { return values } } return append(values, candidate) } // ── group-role: 群身份管理 (im) ────────────────────────────── // ChatRoleList lists custom group roles (list_custom_group_roles, im). var ChatRoleList = shortcut.Shortcut{ Service: "chat", Command: "+chat-role-list", Product: "im", Description: "拉取会话的群身份列表", Intent: "当你想查看某群自定义的群身份(如'班长''值日')都有哪些时使用;需传群 openConversationId,只读返回群身份列表及 openRoleId。", 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_role_list", CanonicalPath: "chat.shortcut_chat_role_list", CLIPath: "chat +chat-role-list", PrimaryCLIPath: "chat +chat-role-list", }, 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{"当你想查看某群自定义的群身份(如'班长''值日')都有哪些时使用;需传群 openConversationId,只读返回群身份列表及 openRoleId。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-role-list --group "}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, }, Tips: []string{`dws chat +chat-role-list --group `}, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("im", "list_custom_group_roles", map[string]any{"openConversationId": rt.Str("group")}) if err != nil { return err } roles := chatRoleListProject(data) return rt.Output(map[string]any{"count": len(roles), "roles": roles}) }, } // chatRoleListProject reshapes list_custom_group_roles into a clean group-role // list ({openRoleId, name}) — clean output projection. List // container and per-item field names are probed defensively across candidate // keys so shape drift yields an empty list rather than a crash or fabricated // data. func chatRoleListProject(data map[string]any) []map[string]any { raw := chatGroupResolveList(data) out := make([]map[string]any, 0, len(raw)) for _, item := range raw { m, ok := item.(map[string]any) if !ok { continue } row := map[string]any{} if v, ok := chatGroupFirst(m, "openRoleId", "open_role_id", "roleId", "id"); ok { row["openRoleId"] = v } if v, ok := chatGroupFirst(m, "name", "roleName", "title"); ok { row["name"] = v } if len(row) > 0 { out = append(out, row) } } return out } // ChatRoleAdd adds a custom group role (add_custom_group_role, im). var ChatRoleAdd = shortcut.Shortcut{ Service: "chat", Command: "+chat-role-add", Product: "im", Description: "添加群身份", Intent: "当你想在群里新增一个自定义群身份/头衔时使用;会实际创建群身份,需传群 openConversationId 和身份名称。", 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_chat_role_add", CanonicalPath: "chat.shortcut_chat_role_add", CLIPath: "chat +chat-role-add", PrimaryCLIPath: "chat +chat-role-add", }, 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{"当你想在群里新增一个自定义群身份/头衔时使用;会实际创建群身份,需传群 openConversationId 和身份名称。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-role-add --group --name \"管理员\""}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "name", Type: shortcut.FlagString, Desc: "群身份名称", Required: true}, }, Tips: []string{`dws chat +chat-role-add --group --name "管理员"`}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("add_custom_group_role", map[string]any{ "openConversationId": rt.Str("group"), "name": rt.Str("name"), }) }, } // ChatRoleUpdate renames a custom group role (update_custom_group_role, im). var ChatRoleUpdate = shortcut.Shortcut{ Service: "chat", Command: "+chat-role-update", Product: "im", Description: "更新群身份名称", Intent: "当你想重命名已有的群身份时使用;会实际更新身份名称,需传群 openConversationId、身份 openRoleId 和新名称。", 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_chat_role_update", CanonicalPath: "chat.shortcut_chat_role_update", CLIPath: "chat +chat-role-update", PrimaryCLIPath: "chat +chat-role-update", }, 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{"当你想重命名已有的群身份时使用;会实际更新身份名称,需传群 openConversationId、身份 openRoleId 和新名称。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-role-update --group --role-id --name \"新名称\""}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "role-id", Type: shortcut.FlagString, Desc: "群身份 openRoleId", Required: true}, {Name: "name", Type: shortcut.FlagString, Desc: "群身份新名称", Required: true}, }, Tips: []string{`dws chat +chat-role-update --group --role-id --name "新名称"`}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("update_custom_group_role", map[string]any{ "openConversationId": rt.Str("group"), "openRoleId": rt.Str("role-id"), "name": rt.Str("name"), }) }, } // ChatRoleRemove deletes a custom group role (remove_custom_group_role, im). var ChatRoleRemove = shortcut.Shortcut{ Service: "chat", Command: "+chat-role-remove", Product: "im", Description: "删除群身份", Intent: "当你想删除某个自定义群身份时使用;会实际删除群身份,不可逆,需传群 openConversationId 和身份 openRoleId。", Risk: shortcut.RiskHighWrite, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "role-id", Type: shortcut.FlagString, Desc: "群身份 openRoleId", Required: true}, }, Tips: []string{`dws chat +chat-role-remove --group --role-id `}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("remove_custom_group_role", map[string]any{ "openConversationId": rt.Str("group"), "openRoleId": rt.Str("role-id"), }) }, } // ChatRoleSetUser overwrites a user's group roles (set_custom_user_roles, im). var ChatRoleSetUser = shortcut.Shortcut{ Service: "chat", Command: "+chat-role-set-user", Product: "im", Description: "设置用户的群身份(覆盖该用户的全部群身份)", Intent: "当你想为某成员整体设定其在群内的身份时使用;会实际改写该用户的群身份集合(覆盖其原有全部身份),需传群、用户和 openRoleId 列表(传空则清除全部)。", 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_chat_role_set_user", CanonicalPath: "chat.shortcut_chat_role_set_user", CLIPath: "chat +chat-role-set-user", PrimaryCLIPath: "chat +chat-role-set-user", }, 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{"当你想为某成员整体设定其在群内的身份时使用;会实际改写该用户的群身份集合(覆盖其原有全部身份),需传群、用户和 openRoleId 列表(传空则清除全部)。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-role-set-user --group --user --role-ids roleId1,roleId2"}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "user", Type: shortcut.FlagString, Desc: "用户 userId 或 openDingTalkId", Required: true}, {Name: "role-ids", Type: shortcut.FlagStringSlice, Desc: "群身份 openRoleId 列表(空则清除全部)", Required: true}, }, Tips: []string{`dws chat +chat-role-set-user --group --user --role-ids roleId1,roleId2`}, Execute: func(rt *shortcut.RuntimeContext) error { user := rt.Str("user") params := map[string]any{ "openConversationId": rt.Str("group"), "openRoleIds": rt.StrSlice("role-ids"), } if isOpenID(user) { params["openDingTalkId"] = user } else { params["userId"] = user } return rt.CallMCP("set_custom_user_roles", params) }, } // ChatRoleRemoveUser removes specific roles from a user (remove_custom_user_roles, im). var ChatRoleRemoveUser = shortcut.Shortcut{ Service: "chat", Command: "+chat-role-remove-user", Product: "im", Description: "移除用户的指定群身份", Intent: "当你只想撤销某成员的部分群身份、保留其余时使用;会实际移除指定的群身份,需传群、用户和要移除的 openRoleId 列表。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "user", Type: shortcut.FlagString, Desc: "用户 userId 或 openDingTalkId", Required: true}, {Name: "role-ids", Type: shortcut.FlagStringSlice, Desc: "要移除的群身份 openRoleId 列表", Required: true}, }, Tips: []string{`dws chat +chat-role-remove-user --group --user --role-ids roleId1`}, Execute: func(rt *shortcut.RuntimeContext) error { user := rt.Str("user") params := map[string]any{ "openConversationId": rt.Str("group"), "openRoleIds": rt.StrSlice("role-ids"), } if isOpenID(user) { params["openDingTalkId"] = user } else { params["userId"] = user } return rt.CallMCP("remove_custom_user_roles", params) }, } // ChatRoleQueryUser queries a member's group roles (query_custom_user_roles, im). var ChatRoleQueryUser = shortcut.Shortcut{ Service: "chat", Command: "+chat-role-query-user", Product: "im", Description: "查询群成员的群身份", Intent: "当你想查看某个群成员当前拥有哪些群身份时使用;只读,需传群 openConversationId 和用户 userId 或 openDingTalkId。", 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_role_query_user", CanonicalPath: "chat.shortcut_chat_role_query_user", CLIPath: "chat +chat-role-query-user", PrimaryCLIPath: "chat +chat-role-query-user", }, 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{"当你想查看某个群成员当前拥有哪些群身份时使用;只读,需传群 openConversationId 和用户 userId 或 openDingTalkId。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +chat-role-query-user --group --user "}, }, }, Flags: []shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true}, {Name: "user", Type: shortcut.FlagString, Desc: "用户 userId 或 openDingTalkId", Required: true}, }, Tips: []string{`dws chat +chat-role-query-user --group --user `}, Execute: func(rt *shortcut.RuntimeContext) error { user := rt.Str("user") params := map[string]any{"openConversationId": rt.Str("group")} if isOpenID(user) { params["openDingTalkId"] = user } else { params["userId"] = user } return rt.CallMCP("query_custom_user_roles", params) }, } func init() { shortcut.Register(withReviewedChatShortcutContracts( ChatSearch, ChatMembersGet, ChatTransferOwner, ChatInviteURL, ChatQuit, ChatUpdateIcon, ChatUpdateSettings, ChatDismiss, ChatSetHistory, ChatUpdateNick, ChatUpdateAlias, ChatListMine, ChatListAll, ChatListJoinRequests, ChatAuditJoin, ChatGetByID, ChatAddBot, ChatBots, ChatRemoveBot, ChatSetAdmin, ChatMute, ChatMuteMember, ChatRoleList, ChatRoleAdd, ChatRoleUpdate, ChatRoleRemove, ChatRoleSetUser, ChatRoleRemoveUser, ChatRoleQueryUser, )...) }