223 lines
8.3 KiB
Go
223 lines
8.3 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 smart
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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"
|
|
)
|
|
|
|
// DeptMembers: list the members of a department, by NAME, in one command.
|
|
//
|
|
// Steps: search the directory for the department by keyword
|
|
// (search_dept_by_keyword) → defensively parse the unique deptId out of the
|
|
// response → print that department's direct members
|
|
// (get_dept_members_by_deptId). Replaces the manual dance of
|
|
// `contact dept search --query <名称>` → copy deptId →
|
|
// `contact dept list-members --depts <deptId>`.
|
|
//
|
|
// Disambiguation: if the name matches zero or several departments, it does NOT
|
|
// guess — it errors and lists the candidates (name + deptId) so the caller can
|
|
// re-run against a specific department.
|
|
//
|
|
// Scope note: get_dept_members_by_deptId returns only the direct members of the
|
|
// resolved department, it does NOT recurse into sub-departments.
|
|
//
|
|
// dws contact +dept-members --dept 技术部
|
|
var DeptMembers = shortcut.Shortcut{
|
|
Service: "contact",
|
|
Command: "+dept-members",
|
|
Product: "contact",
|
|
Description: "按部门名列出部门成员(自动解析 deptId)",
|
|
Intent: "当你只知道某个部门的名称、想知道该部门里都有哪些成员时使用;" +
|
|
"内部先按部门名搜索出唯一的 deptId(匹配到 0 个或多个时不猜测," +
|
|
"而是列出候选部门名与 deptId 让你选),再打印该部门的直接成员列表" +
|
|
"(仅本部门,不递归下级部门)。只读,不做任何修改。",
|
|
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_dept_members",
|
|
CanonicalPath: "contact.shortcut_dept_members",
|
|
CLIPath: "contact +dept-members",
|
|
PrimaryCLIPath: "contact +dept-members",
|
|
},
|
|
Description: "按部门名列出部门成员(自动解析 deptId)",
|
|
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: "按部门名列出部门成员(自动解析 deptId)",
|
|
UseWhen: []string{"当你只知道某个部门的名称、想知道该部门里都有哪些成员时使用;内部先按部门名搜索出唯一的 deptId(匹配到 0 个或多个时不猜测,而是列出候选部门名与 deptId 让你选),再打印该部门的直接成员列表(仅本部门,不递归下级部门)。只读,不做任何修改。"},
|
|
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
|
|
Examples: []string{"dws contact +dept-members --dept 技术部"},
|
|
},
|
|
},
|
|
Flags: []shortcut.Flag{
|
|
{Name: "dept", Type: shortcut.FlagString, Desc: "部门名/关键词", Required: true},
|
|
},
|
|
Tips: []string{`dws contact +dept-members --dept 技术部`},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
if err := rt.RequireAll("dept"); err != nil {
|
|
return err
|
|
}
|
|
keyword := strings.TrimSpace(rt.Str("dept"))
|
|
|
|
// Step 1 — search the directory for the department by keyword.
|
|
data, err := rt.CallMCPData("contact", "search_dept_by_keyword", map[string]any{
|
|
"query": keyword,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 2 — defensively pull the matching departments out of the
|
|
// response and require exactly one, otherwise disambiguate.
|
|
depts := deptMembersExtractDepts(data)
|
|
switch {
|
|
case len(depts) == 0:
|
|
return apperrors.NewValidation(fmt.Sprintf(
|
|
"没找到名字里包含 %q 的部门;换个更准确的部门名再试。", keyword))
|
|
case len(depts) > 1:
|
|
return apperrors.NewValidation(fmt.Sprintf(
|
|
"%q 匹配到 %d 个部门:%s。请用更精确的部门名,"+
|
|
"或直接用 dws contact dept list-members --depts <deptId>。",
|
|
keyword, len(depts), strings.Join(deptMembersLabels(depts), "、")))
|
|
}
|
|
|
|
// Step 3 — list that department's direct members.
|
|
// get_dept_members_by_deptId expects deptIds as a string list.
|
|
return rt.CallMCP("get_dept_members_by_deptId", map[string]any{
|
|
"deptIds": []string{strconv.FormatInt(depts[0].id, 10)},
|
|
})
|
|
},
|
|
}
|
|
|
|
// deptMembersDept is the minimal identity parsed out of a department search hit.
|
|
type deptMembersDept struct {
|
|
id int64
|
|
name string
|
|
}
|
|
|
|
// deptMembersExtractDepts walks the search_dept_by_keyword response and returns
|
|
// the departments it can find. The real-machine payload may wrap the list under
|
|
// a few different keys ({"result": [...]}, {"data": [...]}, or a bare list), so
|
|
// probe several candidate shapes before giving up. Each hit needs a numeric
|
|
// deptId; the deptName is best-effort for the disambiguation message.
|
|
func deptMembersExtractDepts(data map[string]any) []deptMembersDept {
|
|
var out []deptMembersDept
|
|
seen := map[int64]bool{}
|
|
for _, item := range deptMembersCandidateItems(data) {
|
|
id, ok := deptMembersToInt64(deptMembersFirst(item, "deptId", "dept_id", "id"))
|
|
if !ok || seen[id] {
|
|
continue
|
|
}
|
|
seen[id] = true
|
|
name, _ := deptMembersFirst(item, "deptName", "dept_name", "name").(string)
|
|
// search_dept_by_keyword wraps the matched keyword in <red>…</red>
|
|
// highlight markup; strip it so the disambiguation message is clean.
|
|
out = append(out, deptMembersDept{id: id, name: stripHighlightTags(name)})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// deptMembersCandidateItems flattens the several shapes the dept-search payload
|
|
// may take into a flat list of department-object maps to probe.
|
|
func deptMembersCandidateItems(data map[string]any) []map[string]any {
|
|
var out []map[string]any
|
|
if data == nil {
|
|
return out
|
|
}
|
|
appendList := func(v any) {
|
|
if list, ok := v.([]any); ok {
|
|
for _, item := range list {
|
|
if m, ok := item.(map[string]any); ok {
|
|
out = append(out, m)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, key := range []string{"result", "data", "deptList", "departments", "list", "items"} {
|
|
appendList(data[key])
|
|
}
|
|
// The object itself may already be a single department record.
|
|
if _, ok := deptMembersToInt64(deptMembersFirst(data, "deptId", "dept_id", "id")); ok {
|
|
out = append(out, data)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// deptMembersFirst returns the first non-nil value among the given keys.
|
|
func deptMembersFirst(m map[string]any, keys ...string) any {
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok && v != nil {
|
|
return v
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// deptMembersToInt64 coerces a JSON-decoded numeric/string deptId to int64.
|
|
func deptMembersToInt64(v any) (int64, bool) {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return int64(n), true
|
|
case int64:
|
|
return n, true
|
|
case int:
|
|
return int64(n), true
|
|
case json.Number:
|
|
if i, err := n.Int64(); err == nil {
|
|
return i, true
|
|
}
|
|
case string:
|
|
if i, err := strconv.ParseInt(strings.TrimSpace(n), 10, 64); err == nil {
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// deptMembersLabels renders "<deptName>(<deptId>)" candidates for the
|
|
// disambiguation message.
|
|
func deptMembersLabels(depts []deptMembersDept) []string {
|
|
out := make([]string, 0, len(depts))
|
|
for _, d := range depts {
|
|
name := d.name
|
|
if name == "" {
|
|
name = "(未命名)"
|
|
}
|
|
out = append(out, fmt.Sprintf("%s(%d)", name, d.id))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func init() {
|
|
shortcut.Register(DeptMembers)
|
|
}
|