// 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 ( "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" ) // BotSearch searches robots created by the current user (search_my_robots, bot). var BotSearch = shortcut.Shortcut{ Service: "chat", Command: "+bot-search", Product: "bot", Description: "搜索当前用户自己创建的机器人", Intent: "当你要管理或复用自己创建的机器人(比如查到其 robotCode 以便让它进群或发消息)时使用;按机器人名称模糊搜索,只返回当前用户名下创建的机器人列表。", 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_bot_search", CanonicalPath: "chat.shortcut_bot_search", CLIPath: "chat +bot-search", PrimaryCLIPath: "chat +bot-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{"当你要管理或复用自己创建的机器人(比如查到其 robotCode 以便让它进群或发消息)时使用;按机器人名称模糊搜索,只返回当前用户名下创建的机器人列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +bot-search --page 1 --name \"日报\""}, }, }, Flags: []shortcut.Flag{ {Name: "page", Type: shortcut.FlagInt, Default: "1", Desc: "页码"}, {Name: "size", Type: shortcut.FlagInt, Desc: "每页数量"}, {Name: "name", Type: shortcut.FlagString, Desc: "robotName 模糊匹配"}, }, Tips: []string{`dws chat +bot-search --page 1 --name "日报"`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"currentPage": rt.Int("page")} if rt.Int("size") > 0 { params["pageSize"] = rt.Int("size") } if rt.Str("name") != "" { params["robotName"] = rt.Str("name") } data, err := rt.CallMCPData("bot", "search_my_robots", params) if err != nil { return err } robots := botSearchProject(data) return rt.Output(map[string]any{"count": len(robots), "robots": robots}) }, } // botSearchProject reshapes the raw search_my_robots response into a clean robot // list (robotCode/robotName/status) — clean output projection. The // list container and field names are probed defensively across candidate keys. func botSearchProject(data map[string]any) []map[string]any { raw := botResolveList(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 := botFirst(m, "robotCode", "robot_code", "code", "id"); ok { row["robotCode"] = v } if v, ok := botFirst(m, "robotName", "robot_name", "name"); ok { row["robotName"] = v } if v, ok := botFirst(m, "status", "robotStatus", "state"); ok { row["status"] = v } if v, ok := botFirst(m, "gmtCreate", "createTime", "create_time"); ok { row["gmtCreate"] = v } if len(row) > 0 { out = append(out, row) } } return out } // botResolveList locates the list payload inside the response, tolerating a bare // top-level array or nesting under result/data/list/items containers. func botResolveList(data map[string]any) []any { for _, key := range []string{"result", "data", "list", "items", "robots", "bots", "records"} { 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", "robots", "bots", "records", "result", "data"} { if arr, ok := inner[ik].([]any); ok { return arr } } } } return []any{} } // botFirst returns the first present candidate key's value. func botFirst(m map[string]any, keys ...string) (any, bool) { for _, k := range keys { if v, ok := m[k]; ok { return v, true } } return nil, false } // BotFind searches all available robots (search_bots, bot). var BotFind = shortcut.Shortcut{ Service: "chat", Command: "+bot-find", Product: "bot", Description: "搜索全部可用机器人(含他人/官方,返回 openDingTalkId 可发单聊)", Intent: "当你想找到平台上任意可用机器人(含他人创建或官方助手,例如某个日报/审批机器人)以便与其发起单聊时使用;输入关键词,返回含 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_bot_find", CanonicalPath: "chat.shortcut_bot_find", CLIPath: "chat +bot-find", PrimaryCLIPath: "chat +bot-find", }, 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{"当你想找到平台上任意可用机器人(含他人创建或官方助手,例如某个日报/审批机器人)以便与其发起单聊时使用;输入关键词,返回含 openDingTalkId 的机器人列表。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws chat +bot-find --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: "每页返回数量"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标,翻页传 nextCursor"}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"query", "keyword"}}, }, Tips: []string{`dws chat +bot-find --query "日报"`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"keyword": rt.StrFirst("query", "keyword")} if rt.Int("limit") > 0 { params["limit"] = rt.Int("limit") } if rt.Str("cursor") != "" { params["cursor"] = rt.Str("cursor") } data, err := rt.CallMCPData("bot", "search_bots", params) if err != nil { return err } bots := botFindProject(data) return rt.Output(map[string]any{"count": len(bots), "bots": bots}) }, } // botFindProject reshapes the raw search_bots response into a clean bot list // (openDingTalkId/name/robotCode) — clean output projection. The // list container and field names are probed defensively across candidate keys. func botFindProject(data map[string]any) []map[string]any { raw := botResolveList(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 := botFirst(m, "botOpenDingTalkId", "openDingTalkId", "open_ding_talk_id", "openId"); ok { row["openDingTalkId"] = v } if v, ok := botFirst(m, "name", "robotName", "botName"); ok { row["name"] = v } if v, ok := botFirst(m, "robotCode", "robot_code", "code", "id"); ok { row["robotCode"] = v } if len(row) > 0 { out = append(out, row) } } return out } // SearchCommonGroups searches groups shared with given people (search_common_groups, chat server). func init() { shortcut.Register(withReviewedChatShortcutContracts( BotSearch, BotFind, )...) }