734 lines
29 KiB
Go
734 lines
29 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 contact provides declarative shortcuts for the DingTalk contact
|
||
// (通讯录) service: user / department / role / relation queries and the HR
|
||
// roster (花名册) lookups. Each shortcut maps 1:1 onto an MCP tool declared in
|
||
// internal/helpers/contact.go.
|
||
package contact
|
||
|
||
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"
|
||
)
|
||
|
||
// GetSelf 获取当前登录用户信息(我是谁 / 本人)。
|
||
// ListFollowings 获取当前用户的特别关注列表。
|
||
var ListFollowings = shortcut.Shortcut{
|
||
Service: "contact",
|
||
Command: "+list-followings",
|
||
Product: "contact",
|
||
Description: "获取当前用户的特别关注列表",
|
||
Intent: "当你想查看本人在通讯录里「特别关注」的联系人名单(例如常打交道的同事、上级)时使用;无需输入,返回关注对象的用户列表,可用于快速定位这些人的 userId 再发消息或排日程。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "contact",
|
||
Name: "shortcut_list_followings",
|
||
CanonicalPath: "contact.shortcut_list_followings",
|
||
CLIPath: "contact +list-followings",
|
||
PrimaryCLIPath: "contact +list-followings",
|
||
},
|
||
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{"当你想查看本人在通讯录里「特别关注」的联系人名单(例如常打交道的同事、上级)时使用;无需输入,返回关注对象的用户列表,可用于快速定位这些人的 userId 再发消息或排日程。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws contact +list-followings"},
|
||
},
|
||
},
|
||
Tips: []string{
|
||
`dws contact +list-followings`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
// Project the raw {arguments,result:{models:[…]}} envelope down to a
|
||
// clean {count, followings:[{openDingTalkId}]} list.
|
||
data, err := rt.CallMCPData("contact", "list_my_followings", nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
followings := listFollowingsProject(data)
|
||
return rt.Output(map[string]any{"count": len(followings), "followings": followings})
|
||
},
|
||
}
|
||
|
||
// listFollowingsProject reshapes a list_my_followings response into a clean
|
||
// list, unwrapping the result.models container the gateway uses.
|
||
func listFollowingsProject(data map[string]any) []map[string]any {
|
||
var raw []any
|
||
if res, ok := data["result"].(map[string]any); ok {
|
||
raw, _ = res["models"].([]any)
|
||
}
|
||
if raw == nil {
|
||
for _, k := range []string{"models", "list", "items"} {
|
||
if arr, ok := data[k].([]any); ok {
|
||
raw = arr
|
||
break
|
||
}
|
||
}
|
||
}
|
||
out := make([]map[string]any, 0, len(raw))
|
||
for _, it := range raw {
|
||
m, ok := it.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
row := map[string]any{}
|
||
for _, k := range []string{"openDingTalkId", "openDingtalkId", "userId", "name"} {
|
||
if v, ok := m[k]; ok && v != nil {
|
||
row[k] = v
|
||
}
|
||
}
|
||
if len(row) > 0 {
|
||
out = append(out, row)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// SearchUser 按关键词搜索通讯录用户。
|
||
var SearchUser = shortcut.Shortcut{
|
||
Service: "contact",
|
||
Command: "+search-user",
|
||
Product: "contact",
|
||
Description: "按关键词搜索通讯录用户",
|
||
Intent: "当你只知道某人的姓名、花名或部分名字,需要把它解析成 userId 及部门等信息以便后续发消息、排日程或指派任务时使用;输入搜索关键词(--query),返回匹配的用户列表。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "contact",
|
||
Name: "shortcut_search_user",
|
||
CanonicalPath: "contact.shortcut_search_user",
|
||
CLIPath: "contact +search-user",
|
||
PrimaryCLIPath: "contact +search-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{"当你只知道某人的姓名、花名或部分名字,需要把它解析成 userId 及部门等信息以便后续发消息、排日程或指派任务时使用;输入搜索关键词(--query),返回匹配的用户列表。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws contact +search-user --query \"张三\""},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词", Required: true},
|
||
},
|
||
Tips: []string{
|
||
`dws contact +search-user --query "张三"`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
data, err := rt.CallMCPData("contact", "search_contact_by_key_word", map[string]any{
|
||
"keyword": rt.Str("query"),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
users := searchUserProject(data)
|
||
return rt.Output(map[string]any{"count": len(users), "users": users})
|
||
},
|
||
}
|
||
|
||
// searchUserProject reshapes the raw search_contact_by_key_word response into a
|
||
// clean, stable user list (name/userId/flowerName/openDingTalkId/title) — the
|
||
// the clean output projection applied to every list command.
|
||
// Field names are probed defensively across candidate keys.
|
||
func searchUserProject(data map[string]any) []map[string]any {
|
||
var raw []any
|
||
switch result := data["result"].(type) {
|
||
case []any:
|
||
raw = result
|
||
case map[string]any:
|
||
raw = []any{result}
|
||
default:
|
||
return []map[string]any{}
|
||
}
|
||
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{}
|
||
for _, k := range []string{"name", "userId", "flowerName", "openDingTalkId", "title"} {
|
||
if v, ok := m[k]; ok {
|
||
row[k] = v
|
||
}
|
||
}
|
||
if len(row) > 0 {
|
||
out = append(out, row)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// SearchMobile 按手机号搜索通讯录用户。
|
||
var SearchMobile = shortcut.Shortcut{
|
||
Service: "contact",
|
||
Command: "+search-mobile",
|
||
Product: "contact",
|
||
Description: "按手机号搜索通讯录用户",
|
||
Intent: "当你手里只有某人的手机号、需要反查出对应的通讯录用户和 userId 时使用;输入手机号(--mobile),返回该手机号所属的用户信息,适合从电话或名片信息定位到具体员工。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "contact",
|
||
Name: "shortcut_search_mobile",
|
||
CanonicalPath: "contact.shortcut_search_mobile",
|
||
CLIPath: "contact +search-mobile",
|
||
PrimaryCLIPath: "contact +search-mobile",
|
||
},
|
||
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{"当你手里只有某人的手机号、需要反查出对应的通讯录用户和 userId 时使用;输入手机号(--mobile),返回该手机号所属的用户信息,适合从电话或名片信息定位到具体员工。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws contact +search-mobile --mobile 13800138000"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "mobile", Type: shortcut.FlagString, Desc: "手机号", Required: true},
|
||
},
|
||
Tips: []string{
|
||
`dws contact +search-mobile --mobile 13800138000`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
data, err := rt.CallMCPData("contact", "search_user_by_mobile", map[string]any{
|
||
"mobile": rt.Str("mobile"),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
users := searchUserProject(data)
|
||
return rt.Output(map[string]any{"count": len(users), "users": users})
|
||
},
|
||
}
|
||
|
||
// GetUser 批量获取用户详情(组织管理信息:部门、主管、管理员权限)。
|
||
// ListRoles 获取企业所有角色(标签)列表。
|
||
var ListRoles = shortcut.Shortcut{
|
||
Service: "contact",
|
||
Command: "+list-roles",
|
||
Product: "contact",
|
||
Description: "获取企业所有角色(标签)列表",
|
||
Intent: "当你想总览企业里都有哪些角色/员工标签(如「管理员」「财务」「销售」)及其角色 ID 时使用;无需输入,返回全量角色列表,常用于按角色圈定人群前先摸清有哪些角色可选。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "contact",
|
||
Name: "shortcut_list_roles",
|
||
CanonicalPath: "contact.shortcut_list_roles",
|
||
CLIPath: "contact +list-roles",
|
||
PrimaryCLIPath: "contact +list-roles",
|
||
},
|
||
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 时使用;无需输入,返回全量角色列表,常用于按角色圈定人群前先摸清有哪些角色可选。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws contact +list-roles"},
|
||
},
|
||
},
|
||
Tips: []string{
|
||
`dws contact +list-roles`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
data, err := rt.CallMCPData("contact", "get_org_labels", map[string]any{})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
roles := listRolesProject(data)
|
||
return rt.Output(map[string]any{"count": len(roles), "roles": roles})
|
||
},
|
||
}
|
||
|
||
// listRolesProject reshapes the raw get_org_labels response into a clean
|
||
// role/label list ({labelId, labelName}) — clean output projection.
|
||
// The list container and field names are probed defensively across candidate
|
||
// keys so the projection tolerates response-shape drift.
|
||
func listRolesProject(data map[string]any) []map[string]any {
|
||
raw := listRolesResolveList(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 := listRolesFirst(m, "labelId", "label_id", "id"); ok {
|
||
row["labelId"] = v
|
||
}
|
||
if v, ok := listRolesFirst(m, "labelName", "label_name", "name"); ok {
|
||
row["labelName"] = v
|
||
}
|
||
if len(row) > 0 {
|
||
out = append(out, row)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// listRolesResolveList locates the list payload inside the response, tolerating
|
||
// a bare top-level array or nesting under result/data/list/items containers.
|
||
func listRolesResolveList(data map[string]any) []any {
|
||
for _, key := range []string{"result", "data", "list", "items", "labels"} {
|
||
v, ok := data[key]
|
||
if !ok {
|
||
continue
|
||
}
|
||
if arr, ok := v.([]any); ok {
|
||
return listRolesFlattenGroups(arr)
|
||
}
|
||
// container may itself wrap the list one level deeper
|
||
if inner, ok := v.(map[string]any); ok {
|
||
for _, ik := range []string{"list", "items", "labels", "result", "data"} {
|
||
if arr, ok := inner[ik].([]any); ok {
|
||
return listRolesFlattenGroups(arr)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return []any{}
|
||
}
|
||
|
||
// listRolesFlattenGroups flattens the grouped get_org_labels response into a
|
||
// flat list of label objects. get_org_labels returns result[] as label GROUPS
|
||
// ({groupName, labels[]}), nesting the actual roles one level under each group's
|
||
// labels[]. A naive top-level scan would hand back the group wrappers — which
|
||
// carry no labelId/name — so every row would silently project to empty. When
|
||
// the array is already a flat label list (no per-group labels[]), it is returned
|
||
// unchanged so the projection tolerates both response shapes.
|
||
func listRolesFlattenGroups(arr []any) []any {
|
||
flattened := make([]any, 0, len(arr))
|
||
sawGroup := false
|
||
for _, item := range arr {
|
||
m, ok := item.(map[string]any)
|
||
if !ok {
|
||
flattened = append(flattened, item)
|
||
continue
|
||
}
|
||
var nested []any
|
||
for _, gk := range []string{"labels", "subLabels"} {
|
||
if inner, ok := m[gk].([]any); ok {
|
||
nested = inner
|
||
break
|
||
}
|
||
}
|
||
if nested != nil {
|
||
sawGroup = true
|
||
flattened = append(flattened, nested...)
|
||
continue
|
||
}
|
||
flattened = append(flattened, item)
|
||
}
|
||
if sawGroup {
|
||
return flattened
|
||
}
|
||
return arr
|
||
}
|
||
|
||
// listRolesFirst returns the first present candidate key's value.
|
||
func listRolesFirst(m map[string]any, keys ...string) (any, bool) {
|
||
for _, k := range keys {
|
||
if v, ok := m[k]; ok {
|
||
return v, true
|
||
}
|
||
}
|
||
return nil, false
|
||
}
|
||
|
||
// SearchRole 根据角色名称精确匹配查询角色(角色ID、名称)。
|
||
// ListRoleMembers 根据角色 ID 查询该角色下的成员列表。
|
||
var ListRoleMembers = shortcut.Shortcut{
|
||
Service: "contact",
|
||
Command: "+list-role-members",
|
||
Product: "contact",
|
||
Description: "查询角色下的成员列表",
|
||
Intent: "当你已知某个角色 ID、想列出该角色(标签)下的全部成员以便群发通知或统计人群时使用;输入角色 ID(--id),返回该角色下的用户列表,通常先用 +search-role 拿到角色 ID 再调用。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "contact",
|
||
Name: "shortcut_list_role_members",
|
||
CanonicalPath: "contact.shortcut_list_role_members",
|
||
CLIPath: "contact +list-role-members",
|
||
PrimaryCLIPath: "contact +list-role-members",
|
||
},
|
||
Description: "查询角色下的成员列表",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "查询角色下的成员列表",
|
||
UseWhen: []string{"当你已知某个角色 ID、想列出该角色(标签)下的全部成员以便群发通知或统计人群时使用;输入角色 ID(--id),返回该角色下的用户列表,通常先用 +search-role 拿到角色 ID 再调用。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws contact +list-role-members --id 12345"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "id", Type: shortcut.FlagString, Desc: "角色 ID", Required: true},
|
||
},
|
||
Tips: []string{
|
||
`dws contact +list-role-members --id 12345`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
data, err := rt.CallMCPData("contact", "get_label_members_by_labelId", map[string]any{
|
||
"labelId": rt.Str("id"),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
members := memberListProject(data)
|
||
return rt.Output(map[string]any{"count": len(members), "members": members})
|
||
},
|
||
}
|
||
|
||
// memberListProject reshapes a user/member list response into a clean
|
||
// {userId, name} list — clean output projection. Both the list
|
||
// container and the per-item field names are probed defensively across
|
||
// candidate keys, so an empty/unknown shape yields an empty list rather than a
|
||
// crash or fabricated data. Shared by role-member and dept-member listings.
|
||
func memberListProject(data map[string]any) []map[string]any {
|
||
if data == nil {
|
||
return []map[string]any{}
|
||
}
|
||
raw := memberListFindList(data)
|
||
if raw == nil {
|
||
for _, container := range []string{"result", "data"} {
|
||
if inner, ok := data[container].(map[string]any); ok {
|
||
if r := memberListFindList(inner); r != nil {
|
||
raw = r
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out := make([]map[string]any, 0, len(raw))
|
||
for _, item := range raw {
|
||
m, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
// get_dept_members_by_deptId wraps each member under userInfo
|
||
// (deptUserList item = {"userInfo": {userId, name, ...}}); unwrap it or
|
||
// every row projects empty and +list-dept-members silently returns none.
|
||
if ui, ok := m["userInfo"].(map[string]any); ok {
|
||
m = ui
|
||
}
|
||
row := map[string]any{}
|
||
if v := memberListFirst(m, "userId", "user_id", "userid", "id"); v != nil {
|
||
row["userId"] = v
|
||
}
|
||
if v := memberListFirst(m, "name", "userName", "user_name", "flowerName"); v != nil {
|
||
row["name"] = v
|
||
}
|
||
if len(row) > 0 {
|
||
out = append(out, row)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// memberListFindList returns the first slice found under the common list
|
||
// container keys, or nil when none is present. get_dept_members_by_deptId nests
|
||
// its members under deptUserList, so that key must be probed too.
|
||
func memberListFindList(m map[string]any) []any {
|
||
// get_dept_members_by_deptId nests under deptUserList and
|
||
// get_label_members_by_labelId under labelUserList; both must be probed (and
|
||
// each item unwrapped from userInfo above) or +list-dept-members /
|
||
// +list-role-members silently return empty despite having members.
|
||
for _, k := range []string{"result", "data", "list", "items", "members", "userList", "deptUserList", "labelUserList"} {
|
||
if arr, ok := m[k].([]any); ok {
|
||
return arr
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// memberListFirst returns the value of the first present key among keys.
|
||
func memberListFirst(m map[string]any, keys ...string) any {
|
||
for _, k := range keys {
|
||
if v, ok := m[k]; ok {
|
||
return v
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SearchDept 按关键词搜索部门。
|
||
// ListSubDepts 查看指定部门的子部门。
|
||
var ListSubDepts = shortcut.Shortcut{
|
||
Service: "contact",
|
||
Command: "+list-sub-depts",
|
||
Product: "contact",
|
||
Description: "查看指定部门的子部门",
|
||
Intent: "当你想逐层浏览组织架构、查看某个部门下一级的子部门时使用;输入父部门 ID(--dept,根部门为 1),返回其直属子部门列表,可用于自顶向下遍历部门树。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "contact",
|
||
Name: "shortcut_list_sub_depts",
|
||
CanonicalPath: "contact.shortcut_list_sub_depts",
|
||
CLIPath: "contact +list-sub-depts",
|
||
PrimaryCLIPath: "contact +list-sub-depts",
|
||
},
|
||
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(--dept,根部门为 1),返回其直属子部门列表,可用于自顶向下遍历部门树。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws contact +list-sub-depts --dept 1"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "dept", Type: shortcut.FlagInt, Desc: "部门 ID(钉钉根部门为 1)", Required: true},
|
||
},
|
||
Tips: []string{
|
||
`dws contact +list-sub-depts --dept 1`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
data, err := rt.CallMCPData("contact", "get_sub_depts_by_dept_id", map[string]any{
|
||
"deptId": rt.Int("dept"),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
depts := listSubDeptsProject(data)
|
||
return rt.Output(map[string]any{"count": len(depts), "depts": depts})
|
||
},
|
||
}
|
||
|
||
// listSubDeptsProject reshapes get_sub_depts_by_dept_id into a clean
|
||
// {deptId, deptName} list — clean output projection. Both the list
|
||
// container and the per-item field names are probed defensively across
|
||
// candidate keys, so an empty/unknown shape yields an empty list rather than a
|
||
// crash or fabricated data.
|
||
func listSubDeptsProject(data map[string]any) []map[string]any {
|
||
if data == nil {
|
||
return []map[string]any{}
|
||
}
|
||
// Locate the list container: it may sit at the top level or be nested one
|
||
// level under a common envelope key.
|
||
raw := listSubDeptsFindList(data)
|
||
if raw == nil {
|
||
for _, container := range []string{"result", "data"} {
|
||
if inner, ok := data[container].(map[string]any); ok {
|
||
if r := listSubDeptsFindList(inner); r != nil {
|
||
raw = r
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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 := listSubDeptsFirst(m, "deptId", "dept_id", "id"); v != nil {
|
||
row["deptId"] = v
|
||
}
|
||
if v := listSubDeptsFirst(m, "deptName", "dept_name", "name"); v != nil {
|
||
row["deptName"] = v
|
||
}
|
||
if len(row) > 0 {
|
||
out = append(out, row)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// listSubDeptsFindList returns the first slice found under the common list
|
||
// container keys, or nil when none is present.
|
||
func listSubDeptsFindList(m map[string]any) []any {
|
||
for _, k := range []string{"result", "data", "list", "items"} {
|
||
if arr, ok := m[k].([]any); ok {
|
||
return arr
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// listSubDeptsFirst returns the value of the first present key among keys.
|
||
func listSubDeptsFirst(m map[string]any, keys ...string) any {
|
||
for _, k := range keys {
|
||
if v, ok := m[k]; ok {
|
||
return v
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetDept 获取部门详情(部门 ID、名称、人数)。
|
||
// ListDeptMembers 查看部门成员(仅本部门,不含下级)。
|
||
var ListDeptMembers = shortcut.Shortcut{
|
||
Service: "contact",
|
||
Command: "+list-dept-members",
|
||
Product: "contact",
|
||
Description: "查看部门成员(仅本部门,不含下级)",
|
||
Intent: "当你想列出一个或多个部门本级的员工(不含下级子部门)以便群发通知、统计或指派任务时使用;输入部门 ID 列表(--depts,逗号分隔),返回这些部门下的成员,如需含下级需自行遍历子部门。",
|
||
Risk: shortcut.RiskRead,
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: corecmd.ContractDecl{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "contact",
|
||
Name: "shortcut_list_dept_members",
|
||
CanonicalPath: "contact.shortcut_list_dept_members",
|
||
CLIPath: "contact +list-dept-members",
|
||
PrimaryCLIPath: "contact +list-dept-members",
|
||
},
|
||
Description: "查看部门成员(仅本部门,不含下级)",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "查看部门成员(仅本部门,不含下级)",
|
||
UseWhen: []string{"当你想列出一个或多个部门本级的员工(不含下级子部门)以便群发通知、统计或指派任务时使用;输入部门 ID 列表(--depts,逗号分隔),返回这些部门下的成员,如需含下级需自行遍历子部门。"},
|
||
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
||
Examples: []string{"dws contact +list-dept-members --depts 12345,67890"},
|
||
},
|
||
},
|
||
Flags: []shortcut.Flag{
|
||
{Name: "depts", Type: shortcut.FlagStringSlice, Desc: "部门 ID 列表,逗号分隔", Required: true},
|
||
},
|
||
Tips: []string{
|
||
`dws contact +list-dept-members --depts 12345,67890`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
data, err := rt.CallMCPData("contact", "get_dept_members_by_deptId", map[string]any{
|
||
"deptIds": rt.StrSlice("depts"),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
members := memberListProject(data)
|
||
return rt.Output(map[string]any{"count": len(members), "members": members})
|
||
},
|
||
}
|
||
|
||
// ListRosterFields 查询花名册当前用户有权限的字段列表(hrmregister server)。
|
||
var ListRosterFields = shortcut.Shortcut{
|
||
Service: "contact",
|
||
Command: "+list-roster-fields",
|
||
Product: "hrmregister",
|
||
Description: "查询花名册有权限的字段列表",
|
||
Intent: "当你要查询花名册(HR 档案)信息、需要先知道当前身份有权访问哪些字段及其字段编码(fieldCode)时使用;无需输入,返回可用字段列表,通常作为调用 +get-roster 前的准备步骤以指定 --fields。",
|
||
Risk: shortcut.RiskRead,
|
||
Tips: []string{
|
||
`dws contact +list-roster-fields`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
return rt.CallMCP("list_authorized_roster_fields", map[string]any{})
|
||
},
|
||
}
|
||
|
||
// GetRoster 查询员工花名册字段信息(个人档案,hrmregister server)。
|
||
var GetRoster = shortcut.Shortcut{
|
||
Service: "contact",
|
||
Command: "+get-roster",
|
||
Product: "hrmregister",
|
||
Description: "查询员工花名册字段信息(学历、家庭、银行卡、合同等)",
|
||
Intent: "当你需要查看某员工在 HR 花名册中的详细档案字段(如学历、家庭、银行卡、合同等)时使用;可传员工 ID(--staff-id)和要查的字段编码(--fields,来自 +list-roster-fields),不传则按默认查询,返回授权范围内的花名册信息。",
|
||
Risk: shortcut.RiskRead,
|
||
Flags: []shortcut.Flag{
|
||
{Name: "staff-id", Type: shortcut.FlagString, Desc: "查询员工 ID(可选)"},
|
||
{Name: "fields", Type: shortcut.FlagStringSlice, Desc: "指定字段集合,逗号分隔,可通过 +list-roster-fields 获取(可选)"},
|
||
},
|
||
Tips: []string{
|
||
`dws contact +get-roster --staff-id STAFF_ID`,
|
||
`dws contact +get-roster --staff-id STAFF_ID --fields fieldCode1,fieldCode2`,
|
||
},
|
||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||
params := map[string]any{}
|
||
if rt.Changed("staff-id") {
|
||
params["staffId"] = rt.Str("staff-id")
|
||
}
|
||
if rt.Changed("fields") {
|
||
params["fieldCodeList"] = rt.StrSlice("fields")
|
||
}
|
||
return rt.CallMCP("get_authorized_emp_rosterInfo", params)
|
||
},
|
||
}
|
||
|
||
func init() {
|
||
shortcut.Register(
|
||
ListFollowings,
|
||
SearchUser,
|
||
SearchMobile,
|
||
ListRoles,
|
||
ListRoleMembers,
|
||
ListSubDepts,
|
||
ListDeptMembers,
|
||
ListRosterFields,
|
||
GetRoster,
|
||
)
|
||
}
|