236 lines
9.1 KiB
Go
236 lines
9.1 KiB
Go
// 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,
|
|
)...)
|
|
}
|