// Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package smart import ( "strconv" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" ) // 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: // 1. 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. // 2. defensively project each group's key fields (field names probed across // several candidate keys, since the gateway shape isn't guaranteed); // 3. 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 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: []shortcut.Flag{ {Name: "type", Type: shortcut.FlagString, Desc: "按群类型过滤(可选,如返回中的 groupType/conversationType,大小写不敏感)", Required: false}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页返回数量(默认 200)", Default: "200"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传上次的 nextCursor"}, }, Tips: []string{ `dws chat +my-groups`, `dws chat +my-groups --type group`, }, Execute: func(rt *shortcut.RuntimeContext) error { // Step 1 — list my groups. `limit` mirrors chat.go's list_my_groups_pagination // call site (chat group list-all); pass a generous page size. params := map[string]any{"limit": rt.Int("limit")} if cursor := strings.TrimSpace(rt.Str("cursor")); cursor != "" && cursor != "0" { params["cursor"] = cursor } data, err := rt.CallMCPData("im", "list_my_groups_pagination", params) if err != nil { return err } // Step 2 — project key fields defensively. groups := myGroupsExtract(data) typeFilter := strings.TrimSpace(rt.Str("type")) projected := make([]map[string]any, 0, len(groups)) for _, g := range groups { row := myGroupsProject(g) if typeFilter != "" { gt, _ := row["type"].(string) if !strings.EqualFold(strings.TrimSpace(gt), typeFilter) { continue } } projected = append(projected, row) } payload := map[string]any{ "count": len(projected), "groups": projected, } chatmsg.ApplyPagination(payload, data) return rt.Output(payload) }, } // myGroupsExtract walks a list_my_groups_pagination response and returns its // group entries. The gateway wraps the list under one of several common // container keys, so we probe them (and one nested level) before giving up. func myGroupsExtract(data map[string]any) []map[string]any { for _, key := range []string{"result", "list", "groups", "groupList", "items", "data", "records", "conversations"} { if arr, ok := data[key].([]any); ok { return myGroupsToMaps(arr) } if inner, ok := data[key].(map[string]any); ok { for _, k2 := range []string{"list", "groups", "groupList", "items", "records", "result", "conversations"} { if arr, ok := inner[k2].([]any); ok { return myGroupsToMaps(arr) } } } } return nil } func myGroupsToMaps(arr []any) []map[string]any { out := make([]map[string]any, 0, len(arr)) for _, it := range arr { if m, ok := it.(map[string]any); ok { out = append(out, m) } } return out } // myGroupsProject reshapes a single group into the projected key fields, probing // multiple candidate keys for each because the response shape isn't guaranteed. func myGroupsProject(m map[string]any) map[string]any { row := map[string]any{} if v := myGroupsStr(m, "openConversationId", "openConversationID", "conversationId", "openCid", "cid", "id"); v != "" { row["conversationId"] = v } if v := myGroupsStr(m, "name", "groupName", "title", "conversationName", "chatName"); v != "" { row["name"] = v } if v := myGroupsStr(m, "ownerUserId", "owner", "ownerId", "ownerOpenDingTalkId", "ownerOpenId", "groupOwnerId"); v != "" { row["owner"] = v } if v, ok := myGroupsInt(m, "memberCount", "memberNum", "memberSize", "userCount", "totalMember", "count"); ok { row["memberCount"] = v } if v := myGroupsStr(m, "groupType", "conversationType", "type", "chatType"); v != "" { row["type"] = v } return row } func myGroupsStr(m map[string]any, keys ...string) string { for _, key := range keys { switch v := m[key].(type) { case string: if s := strings.TrimSpace(v); s != "" { return s } case float64: return strconv.FormatFloat(v, 'f', -1, 64) case bool: return strconv.FormatBool(v) } } return "" } func myGroupsInt(m map[string]any, keys ...string) (int64, bool) { for _, key := range keys { switch v := m[key].(type) { case float64: return int64(v), true case string: if n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64); err == nil { return n, true } } } return 0, false } func init() { shortcut.Register(MyGroups) }