313 lines
11 KiB
Go
313 lines
11 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 wiki declares high-fidelity shortcuts for the DingTalk wiki
|
|
// (knowledge base) service: space management, member management and node
|
|
// management. Tool names and parameters mirror internal/helpers/wiki.go.
|
|
package wiki
|
|
|
|
import "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
|
|
|
// ── space (知识库) ────────────────────────────────────────────
|
|
|
|
// SpaceCreate → create_wikiSpace
|
|
// SpaceGet → get_wikiSpace
|
|
// SpaceList → list_wikiSpaces
|
|
var SpaceList = shortcut.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+space-list",
|
|
Product: "wiki",
|
|
Description: "列出组织 / 个人知识库",
|
|
Intent: "当你想浏览自己有权限访问的知识库、拿到目标知识库的 workspaceId 却不确定具体名称时使用;可按类型(组织知识库或我的知识库)分页列出,返回知识库列表,是定位知识库的常用入口。",
|
|
Risk: shortcut.RiskRead,
|
|
Flags: []shortcut.Flag{
|
|
{Name: "type", Type: shortcut.FlagString, Default: "orgWikiSpace", Desc: "知识库类型: orgWikiSpace(默认) / myWikiSpace", Enum: []string{"orgWikiSpace", "myWikiSpace"}},
|
|
{Name: "limit", Type: shortcut.FlagString, Desc: "每页数量 1-50 (默认 20)"},
|
|
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标 (首页留空)"},
|
|
},
|
|
Tips: []string{
|
|
`dws wiki +space-list`,
|
|
`dws wiki +space-list --type myWikiSpace`,
|
|
},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{}
|
|
if rt.Changed("type") {
|
|
params["wikiSpaceType"] = rt.Str("type")
|
|
}
|
|
if rt.Changed("limit") {
|
|
params["pageSize"] = rt.Str("limit")
|
|
}
|
|
if rt.Changed("cursor") {
|
|
params["pageToken"] = rt.Str("cursor")
|
|
}
|
|
data, err := rt.CallMCPData("wiki", "list_wikiSpaces", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
spaces := spaceListProject(data)
|
|
return rt.Output(map[string]any{"count": len(spaces), "spaces": spaces})
|
|
},
|
|
}
|
|
|
|
// spaceListProject reshapes list_wikiSpaces into a clean space list
|
|
// ({workspaceId, name, description, createTime}) — output-projection fidelity
|
|
// for clean output. The list container and per-item field names are probed defensively
|
|
// across candidate keys, so an unrecognized shape yields an empty list.
|
|
func spaceListProject(data map[string]any) []map[string]any {
|
|
raw := wikiSpaceRawList(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 := wikiSpaceFirst(m, "workspaceId", "workspace_id", "spaceId", "space_id", "id"); v != nil {
|
|
row["workspaceId"] = v
|
|
}
|
|
if v := wikiSpaceFirst(m, "name", "title", "spaceName"); v != nil {
|
|
row["name"] = v
|
|
}
|
|
if v := wikiSpaceFirst(m, "description", "desc"); v != nil {
|
|
row["description"] = v
|
|
}
|
|
if v := wikiSpaceFirst(m, "createTime", "create_time", "gmtCreate", "createdAt"); v != nil {
|
|
row["createTime"] = v
|
|
}
|
|
if len(row) > 0 {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// wikiSpaceRawList locates the space array across candidate container keys,
|
|
// tolerating a nested {result|data:{list|items|spaces}} wrapper.
|
|
func wikiSpaceRawList(data map[string]any) []any {
|
|
for _, k := range []string{"result", "data", "list", "items", "spaces", "workspaces"} {
|
|
if arr, ok := data[k].([]any); ok {
|
|
return arr
|
|
}
|
|
if inner, ok := data[k].(map[string]any); ok {
|
|
for _, ik := range []string{"list", "items", "spaces", "workspaces", "result", "data"} {
|
|
if arr, ok := inner[ik].([]any); ok {
|
|
return arr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// wikiSpaceFirst returns the first present value among candidate keys.
|
|
func wikiSpaceFirst(m map[string]any, keys ...string) any {
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok {
|
|
return v
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SpaceSearch → search_wikiSpaces
|
|
var SpaceSearch = shortcut.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+space-search",
|
|
Product: "wiki",
|
|
Description: "搜索知识库",
|
|
Intent: "当你只记得知识库名称的部分关键词、想快速按名称定位某个知识库时使用;输入关键词返回匹配的知识库列表,比逐页 +space-list 更快找到目标 workspaceId。",
|
|
Risk: shortcut.RiskRead,
|
|
Flags: []shortcut.Flag{
|
|
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词", Required: true},
|
|
{Name: "limit", Type: shortcut.FlagString, Desc: "返回数量 1-20 (默认 10)"},
|
|
},
|
|
Tips: []string{`dws wiki +space-search --query "产品文档"`},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{"keyword": rt.Str("query")}
|
|
if rt.Changed("limit") {
|
|
params["pageSize"] = rt.Str("limit")
|
|
}
|
|
data, err := rt.CallMCPData("wiki", "search_wikiSpaces", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
spaces := spaceListProject(data)
|
|
return rt.Output(map[string]any{"count": len(spaces), "spaces": spaces})
|
|
},
|
|
}
|
|
|
|
// SpaceDelete → delete_wikiSpace
|
|
// ── member (知识库成员) ───────────────────────────────────────
|
|
|
|
// MemberAdd → add_member
|
|
// MemberUpdate → update_member
|
|
// MemberList → list_member
|
|
// MemberRemove → remove_member
|
|
// ── node (知识库节点,路由到 doc MCP server) ──────────────────
|
|
|
|
// NodeList → list_nodes (doc)
|
|
var NodeList = shortcut.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+node-list",
|
|
Product: "doc",
|
|
Description: "列出知识库节点",
|
|
Intent: "当你要浏览某个知识库的目录结构、查看某文件夹下有哪些文档/子文件夹并拿到它们的 nodeId 时使用;输入 workspace(可选父节点 folder),分页返回该层级的节点列表,是逐层进入知识库定位文档的常用方式。",
|
|
Risk: shortcut.RiskRead,
|
|
Flags: []shortcut.Flag{
|
|
{Name: "workspace", Type: shortcut.FlagString, Desc: "知识库 ID", Required: true},
|
|
{Name: "folder", Type: shortcut.FlagString, Desc: "父节点 nodeId (不传则列出根目录)"},
|
|
{Name: "limit", Type: shortcut.FlagInt, Desc: "每页数量 (默认 50,最大 50)"},
|
|
{Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标"},
|
|
},
|
|
Tips: []string{`dws wiki +node-list --workspace <workspaceId> --folder <parentNodeId>`},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{"workspaceId": rt.Str("workspace")}
|
|
if rt.Changed("folder") {
|
|
params["folderId"] = rt.Str("folder")
|
|
}
|
|
if rt.Changed("limit") {
|
|
params["pageSize"] = rt.Int("limit")
|
|
}
|
|
if rt.Changed("cursor") {
|
|
params["pageToken"] = rt.Str("cursor")
|
|
}
|
|
data, err := rt.CallMCPData("wiki", "list_nodes", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nodes := nodeListProject(data)
|
|
return rt.Output(map[string]any{"count": len(nodes), "nodes": nodes})
|
|
},
|
|
}
|
|
|
|
// nodeListProject reshapes list_nodes into a clean node list (name/nodeId/type)
|
|
// — clean output projection. Container and field keys are probed
|
|
// defensively across candidate aliases; an unrecognized shape yields an empty list.
|
|
func nodeListProject(data map[string]any) []map[string]any {
|
|
raw := nodeListRawList(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 := nodeListFirst(m, "name", "title", "nodeName"); v != nil {
|
|
row["name"] = v
|
|
}
|
|
if v := nodeListFirst(m, "nodeId", "node_id", "id", "uuid", "dentryUuid"); v != nil {
|
|
row["nodeId"] = v
|
|
}
|
|
if v := nodeListFirst(m, "type", "nodeType", "docType", "fileType"); v != nil {
|
|
row["type"] = v
|
|
}
|
|
if len(row) > 0 {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// nodeListRawList locates the node array across candidate container keys,
|
|
// tolerating a nested {result|data:{list|items|nodes}} wrapper.
|
|
func nodeListRawList(data map[string]any) []any {
|
|
for _, k := range []string{"result", "data", "list", "items", "nodes"} {
|
|
if arr, ok := data[k].([]any); ok {
|
|
return arr
|
|
}
|
|
if inner, ok := data[k].(map[string]any); ok {
|
|
for _, ik := range []string{"list", "items", "nodes", "result", "data"} {
|
|
if arr, ok := inner[ik].([]any); ok {
|
|
return arr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// nodeListFirst returns the first present value among candidate keys.
|
|
func nodeListFirst(m map[string]any, keys ...string) any {
|
|
for _, k := range keys {
|
|
if v, ok := m[k]; ok {
|
|
return v
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NodeCreate → create_file (doc)
|
|
// NodeCopy → copy_document (doc)
|
|
var NodeCopy = shortcut.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+node-copy",
|
|
Product: "doc",
|
|
Description: "复制知识库节点",
|
|
Intent: "当你想基于已有文档/文件夹快速生成一份副本(如用模板起草新文档、留档备份)时使用;指定源 node 和目标 folder,会实际在知识库中复制出一个新节点,原节点保持不变。",
|
|
Risk: shortcut.RiskWrite,
|
|
Flags: []shortcut.Flag{
|
|
{Name: "workspace", Type: shortcut.FlagString, Desc: "知识库 ID", Required: true},
|
|
{Name: "node", Type: shortcut.FlagString, Desc: "源节点 ID", Required: true},
|
|
{Name: "folder", Type: shortcut.FlagString, Desc: "目标文件夹 nodeId (不传则复制到根目录)"},
|
|
},
|
|
Tips: []string{`dws wiki +node-copy --workspace <workspaceId> --node <nodeId> --folder <targetFolderId>`},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{
|
|
"nodeId": rt.Str("node"),
|
|
"workspaceId": rt.Str("workspace"),
|
|
}
|
|
if rt.Changed("folder") {
|
|
params["targetFolderId"] = rt.Str("folder")
|
|
}
|
|
return rt.CallMCP("copy_document", params)
|
|
},
|
|
}
|
|
|
|
// NodeMove → move_document (doc)
|
|
var NodeMove = shortcut.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+node-move",
|
|
Product: "doc",
|
|
Description: "移动知识库节点",
|
|
Intent: "当你要重新整理知识库目录、把某个文档或文件夹从当前位置挪到另一个文件夹(或根目录)下时使用;指定源 node 和目标 folder,会实际改变该节点在知识库中的所属位置。",
|
|
Risk: shortcut.RiskWrite,
|
|
Flags: []shortcut.Flag{
|
|
{Name: "workspace", Type: shortcut.FlagString, Desc: "知识库 ID", Required: true},
|
|
{Name: "node", Type: shortcut.FlagString, Desc: "源节点 ID", Required: true},
|
|
{Name: "folder", Type: shortcut.FlagString, Desc: "目标文件夹 nodeId (不传则移动到根目录)"},
|
|
},
|
|
Tips: []string{`dws wiki +node-move --workspace <workspaceId> --node <nodeId> --folder <targetFolderId>`},
|
|
Execute: func(rt *shortcut.RuntimeContext) error {
|
|
params := map[string]any{
|
|
"nodeId": rt.Str("node"),
|
|
"workspaceId": rt.Str("workspace"),
|
|
}
|
|
if rt.Changed("folder") {
|
|
params["targetFolderId"] = rt.Str("folder")
|
|
}
|
|
return rt.CallMCP("move_document", params)
|
|
},
|
|
}
|
|
|
|
// NodeDelete → delete_document (doc)
|
|
// NodeSearch → search_documents (doc)
|
|
func init() {
|
|
shortcut.Register(
|
|
SpaceList,
|
|
SpaceSearch,
|
|
NodeList,
|
|
NodeCopy,
|
|
NodeMove,
|
|
)
|
|
}
|