package helpers import ( "context" "encoding/json" "fmt" "os" "strconv" "strings" "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/spf13/cobra" ) // ────────────────────────────────────────────────────────── // 跨产品透明路由(Proxy Route) // // 目的: // 解决 LLM Agent 按直觉路径调用不存在命令的问题。例如 Agent 会尝试 // `dws wiki node list`,但该操作实际由 doc 产品实现(`dws doc list`)。 // 这是因为 dws 的命令归属由底层 MCP Server 的 tool 划分决定,而非用户 // 认知模型决定。wiki/doc/drive 三个产品属于同一"文档域",概念交织。 // // 解决方案: // 在源产品(wiki/drive)中注册 hidden 子命令,当被调用时透明转发到 // 目标产品(doc)的对应命令。对调用者完全透明,首次即成功。 // // 适用范围: // 仅 wiki → doc、drive → doc 两条链路。其他 15+ 产品边界清晰,无此问题。 // // 全局搜索标识: // grep "// [PROXY]" 可定位所有路由声明点 // grep "proxySubCmd(" 可定位所有转发命令注册处 // ────────────────────────────────────────────────────────── // proxySubCmd 创建一个 hidden 子命令,被调用时透明转发到目标命令。 // // 与 hintSubCmd 的区别:hintSubCmd 打印提示后退出(Agent 需重试), // proxySubCmd 直接执行目标命令(首次即成功)。 // // 目标命令通过延迟解析获取(运行时从命令树查找),因为 wiki.go 初始化时 // doc 命令尚未挂载到 root 上。 // // 参数: // - use: 子命令名(如 "list"、"read") // - targetProduct: 目标产品名(如 "doc") // - targetPath: 目标子路径,空格分隔(如 "list"、"block insert") // - flagRenames: flag 重命名映射(如 {"workspace": "workspace-ids"}),nil 表示全部透传 func proxySubCmd(use, targetProduct, targetPath string, flagRenames map[string]string) *cobra.Command { return &cobra.Command{ Use: use, Hidden: true, DisableFlagParsing: true, SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { // 延迟解析:运行时从命令树查找目标 root := cmd.Root() var targetCmd *cobra.Command for _, c := range root.Commands() { if c.Name() == targetProduct { targetCmd = c break } } if targetCmd != nil && targetPath != "" { for _, part := range strings.Fields(targetPath) { found := false for _, child := range targetCmd.Commands() { if child.Name() == part { targetCmd = child found = true break } } if !found { targetCmd = nil break } } } if targetCmd == nil { return fmt.Errorf("proxy target not found: dws %s %s", targetProduct, targetPath) } fmt.Fprintf(os.Stderr, "→ redirecting to: %s\n", targetCmd.CommandPath()) // flag 重命名(仅在有映射时分配新 slice) finalArgs := args if len(flagRenames) > 0 { finalArgs = make([]string, len(args)) copy(finalArgs, args) for i, arg := range finalArgs { if !strings.HasPrefix(arg, "--") { continue } flagPart := strings.TrimPrefix(arg, "--") eqIdx := strings.Index(flagPart, "=") var flagName string if eqIdx >= 0 { flagName = flagPart[:eqIdx] } else { flagName = flagPart } if newName, ok := flagRenames[flagName]; ok { if eqIdx >= 0 { finalArgs[i] = "--" + newName + "=" + flagPart[eqIdx+1:] } else { finalArgs[i] = "--" + newName } } } } // 直接调用目标命令的 RunE,绕过 root.Execute() 避免无限递归 if targetCmd.DisableFlagParsing { if targetCmd.RunE != nil { return targetCmd.RunE(targetCmd, finalArgs) } if targetCmd.Run != nil { targetCmd.Run(targetCmd, finalArgs) return nil } } else { if err := targetCmd.ParseFlags(finalArgs); err != nil { return fmt.Errorf("proxy flag parse error for %q: %w", targetCmd.CommandPath(), err) } targetArgs := targetCmd.Flags().Args() if targetCmd.RunE != nil { return targetCmd.RunE(targetCmd, targetArgs) } if targetCmd.Run != nil { targetCmd.Run(targetCmd, targetArgs) return nil } } return fmt.Errorf("proxy target %q has no RunE/Run", targetCmd.CommandPath()) }, } } // ────────────────────────────────────────────────────────── // dws wiki — 知识库 // ────────────────────────────────────────────────────────── func newWikiCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/wiki.json // products.wiki). Catalog assembly stamps provenance contract_final. contract.RegisterProductDecl(contract.ProductDecl{ ID: "wiki", Selection: contract.ProductSelectionDecl{ AgentSummary: "管理钉钉知识库空间、节点与成员权限", UseWhen: []string{ "查找或管理知识库、知识库内节点及知识库成员时", }, AvoidWhen: []string{ "需要编辑在线文档正文时使用 doc;只管理钉盘普通文件时使用 drive", }, }, }) root := &cobra.Command{ Use: "wiki", Short: "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询", Long: `管理钉钉文档知识库:空间管理(创建/查看/列出/搜索/删除)、节点管理(列出/创建/复制/移动/删除)、成员管理(添加/更新/列出/移除)、动态查询(知识库活动动态)。`, RunE: groupRunE, } spaceCmd := &cobra.Command{Use: "space", Short: "知识库管理", RunE: groupRunE} spaceCreateCmd := &cobra.Command{ Use: "create", Short: "创建知识库", Long: `创建一个新的钉钉文档知识库(WikiSpace)。 创建成功后返回新知识库的 workspaceId,可用于后续在该知识库下创建文档或遍历文件。 操作受权限控制,仅当调用者具备在当前组织内创建知识库的权限时可成功创建。`, Example: ` dws wiki space create --name "产品文档库" dws wiki space create --name "技术方案" --desc "团队技术方案归档"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "name"); err != nil { return err } toolArgs := map[string]any{ "name": mustGetFlag(cmd, "name"), } if v := flagOrFallback(cmd, "desc", "description"); v != "" { toolArgs["description"] = v } if v := mustGetFlag(cmd, "icon"); v != "" { toolArgs["icon"] = v } return callMCPTool("create_wikiSpace", toolArgs) }, } DeclareLeafMetadata(spaceCreateCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "create_wikiSpace", CanonicalPath: "wiki.create_wikiSpace", CLIPath: "wiki space create", PrimaryCLIPath: "wiki space create", }, Description: "创建一个新的钉钉文档知识库(WikiSpace)", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "wiki", RPCName: "create_wikiSpace"}, }, Selection: contract.SelectionSpec{ AgentSummary: "创建一个新的钉钉文档知识库(WikiSpace)", UseWhen: []string{"用户要新建一个知识库容器时"}, AvoidWhen: []string{"在已有知识库内建文档/文件夹用 node create,不要反复 create space"}, Examples: []string{ "dws wiki space create --name \"产品文档库\" --format json", "dws wiki space create --name \"技术方案\" --desc \"团队技术方案归档\" --format json", }, }, // name is validated in RunE; publish required via ParamDecl Parameters: []contract.ParamDecl{ {Name: "desc", Property: "description"}, {Name: "name", Required: boolPtr(true)}, }, }, }) spaceGetCmd := &cobra.Command{ Use: "get", Short: "查看知识库详情", Long: `获取指定知识库的详细信息,包括名称、描述、创建者、创建时间、成员数量等。 支持传入知识库 ID 或知识库 URL,系统自动识别。 知识库 URL 格式:https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview`, Example: ` dws wiki space get --workspace dws wiki space get --workspace "https://alidocs.dingtalk.com/i/spaces/xxx/overview"`, RunE: func(cmd *cobra.Command, args []string) error { workspaceID := flagOrFallback(cmd, "workspace", "workspace-id") if workspaceID == "" { return fmt.Errorf("flag --workspace is required") } return callMCPTool("get_wikiSpace", map[string]any{ "workspaceId": workspaceID, }) }, } DeclareLeafMetadata(spaceGetCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "get_wikiSpace", CanonicalPath: "wiki.get_wikiSpace", CLIPath: "wiki space get", PrimaryCLIPath: "wiki space get", }, Description: "获取指定知识库的详细信息,包括名称、描述、创建者、创建时间、成员数量等", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "wiki", RPCName: "get_wikiSpace"}, }, Selection: contract.SelectionSpec{ AgentSummary: "获取指定知识库的详细信息,包括名称、描述、创建者、创建时间、成员数量等", UseWhen: []string{"查看指定知识库详情(名称、描述、创建者等)时"}, AvoidWhen: []string{"列知识库用 list;删库用 space delete(需确认)"}, Examples: []string{"dws wiki space get --workspace --format json"}, }, // workspace is validated in RunE; publish required via ParamDecl Parameters: []contract.ParamDecl{ {Name: "workspace", Property: "workspaceId", Required: boolPtr(true)}, }, }, }) spaceListCmd := &cobra.Command{ Use: "list", Short: "列出空间(知识库 / 钉盘空间)", Long: `获取当前用户有权访问的空间列表。统一管理两种空间类型。 通过 --type 参数控制返回范围: orgWikiSpace — 组织知识库列表(默认,支持分页) myWikiSpace — 当前用户的「我的文档」个人空间(固定 1 条) orgSpace — 钉盘企业空间(团队文件)列表 mySpace — 钉盘「我的文件」个人空间`, Example: ` dws wiki space list dws wiki space list --type myWikiSpace dws wiki space list --type orgWikiSpace --limit 50 dws wiki space list --type orgSpace dws wiki space list --type mySpace`, RunE: func(cmd *cobra.Command, args []string) error { spaceType := mustGetFlag(cmd, "type") // 钉盘空间类型:路由到 drive MCP server if spaceType == "orgSpace" || spaceType == "mySpace" { driveArgs := map[string]any{"spaceType": spaceType} if v := mustGetFlag(cmd, "limit"); v != "" { if n, err := strconv.Atoi(v); err == nil { driveArgs["maxResults"] = n } else { driveArgs["maxResults"] = v } } if v := flagOrFallback(cmd, "cursor", "page-token"); v != "" { driveArgs["nextToken"] = v } return callMCPToolOnServer("drive", "list_spaces", driveArgs) } // 文档知识库类型:原有逻辑 toolArgs := map[string]any{} if spaceType != "" { toolArgs["wikiSpaceType"] = spaceType } if v := mustGetFlag(cmd, "limit"); v != "" { toolArgs["pageSize"] = v } if v := flagOrFallback(cmd, "cursor", "page-token"); v != "" { toolArgs["pageToken"] = v } return callMCPTool("list_wikiSpaces", toolArgs) }, } DeclareLeafMetadata(spaceListCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "list_wikiSpaces", CanonicalPath: "wiki.list_wikiSpaces", CLIPath: "wiki space list", PrimaryCLIPath: "wiki space list", }, Description: "列出当前用户可访问的知识库或钉盘空间", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "The CLI command routes by --type between wiki/list_wikiSpaces and drive/list_spaces, so the reviewed executable wrapper has no single direct MCP interface.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出当前用户可访问的知识库或钉盘空间", UseWhen: []string{ "列出组织知识库(默认)、我的文档(--type myWikiSpace)、钉盘企业空间(--type orgSpace)或我的文件(--type mySpace)时", "需要 workspaceId / rootFolderId 作为后续 node/drive 操作前置时", }, AvoidWhen: []string{ "按名称搜知识库用 space search", "看单个知识库详情用 space get", }, Examples: []string{ "dws wiki space list --format json", "dws wiki space list --type myWikiSpace --format json", }, }, }, }) spaceSearchCmd := &cobra.Command{ Use: "search", Short: "搜索知识库", Long: `根据关键词搜索当前用户有权限访问的知识库列表,匹配知识库名称和描述。`, Example: ` dws wiki space search --query "产品文档" dws wiki space search --query "技术方案" --limit 20`, RunE: func(cmd *cobra.Command, args []string) error { spaceType := mustGetFlag(cmd, "type") // 「我的文档」场景:走 list_wikiSpaces 获取个人空间 if spaceType == "myWikiSpace" { return callMCPTool("list_wikiSpaces", map[string]any{ "wikiSpaceType": "myWikiSpace", }) } // 常规搜索场景:query 必填 query := flagOrFallback(cmd, "query", "keyword") if query == "" { return fmt.Errorf("flag --query is required") } toolArgs := map[string]any{ "keyword": query, } if v := mustGetFlag(cmd, "limit"); v != "" { toolArgs["pageSize"] = v } return callMCPTool("search_wikiSpaces", toolArgs) }, } DeclareLeafMetadata(spaceSearchCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "search_wikiSpaces", CanonicalPath: "wiki.search_wikiSpaces", CLIPath: "wiki space search", PrimaryCLIPath: "wiki space search", }, Description: "根据关键词搜索当前用户有权限访问的知识库列表,匹配知识库名称和描述", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "wiki", RPCName: "search_wikiSpaces"}, }, Selection: contract.SelectionSpec{ AgentSummary: "根据关键词搜索当前用户有权限访问的知识库列表,匹配知识库名称和描述", UseWhen: []string{"按关键词搜索有权访问的组织知识库时"}, AvoidWhen: []string{"直接列全部知识库用 space list;我的文档用 list --type myWikiSpace"}, Examples: []string{"dws wiki space search --query \"产品文档\" --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "limit", Property: "pageSize"}, {Name: "query", Property: "keyword"}, }, }, }) // space create flags spaceCreateCmd.Flags().String("name", "", "知识库名称 (必填,不超过 32 字符)") spaceCreateCmd.Flags().String("desc", "", "知识库描述 (选填,不超过 500 字符)") spaceCreateCmd.Flags().String("icon", "", "知识库图标标识 (选填)") // space get flags — primary is --workspace, consistent with doc commands. // --workspace-id is hidden alias: LLMs derive it from API response field "workspaceId" (camelCase → kebab-case). spaceGetCmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") spaceGetCmd.Flags().String("workspace-id", "", "") _ = spaceGetCmd.Flags().MarkHidden("workspace-id") // space list flags spaceListCmd.Flags().String("type", "orgWikiSpace", "空间类型: orgWikiSpace(默认) / myWikiSpace / orgSpace(钉盘企业空间) / mySpace(钉盘我的文件)") spaceListCmd.Flags().String("limit", "", "每页数量 1-50 (默认 20)") spaceListCmd.Flags().String("cursor", "", "分页游标 (首页留空)") // space search flags spaceSearchCmd.Flags().String("query", "", "搜索关键词 (搜索组织知识库时必填)") spaceSearchCmd.Flags().String("type", "", "知识库类型: myWikiSpace 时直接返回「我的文档」,省略则搜索组织知识库") spaceSearchCmd.Flags().String("limit", "", "返回数量 1-20 (默认 10)") // ── cross-product hidden aliases ── for _, cmd := range []*cobra.Command{spaceCreateCmd, spaceGetCmd, spaceListCmd, spaceSearchCmd} { RegisterCrossProductAliases(cmd) } spaceDeleteCmd := &cobra.Command{ Use: "delete", Short: "删除知识库", Long: `将指定知识库移入回收站。 删除后知识库会进入回收站,可在回收站中恢复(有保留期限)。 支持传入知识库 ID 或知识库 URL,系统自动识别。 知识库 URL 格式:https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview 注意: - 操作者必须具备知识库的 OWNER 角色。 - 这是一个危险操作,执行前请确认。`, Example: ` dws wiki space delete --workspace dws wiki space delete --workspace "https://alidocs.dingtalk.com/i/spaces/xxx/overview"`, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } return callMCPTool("delete_wikiSpace", map[string]any{ "workspaceId": workspaceID, }) }, } DeclareLeafMetadata(spaceDeleteCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "delete_wikiSpace", CanonicalPath: "wiki.delete_wikiSpace", CLIPath: "wiki space delete", PrimaryCLIPath: "wiki space delete", }, Description: "将指定知识库移入回收站", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "wiki", RPCName: "delete_wikiSpace"}, }, Selection: contract.SelectionSpec{ AgentSummary: "将指定知识库移入回收站", UseWhen: []string{"用户明确要求删除整个知识库(移入回收站),且已确认 workspace 与影响范围时"}, AvoidWhen: []string{ "只删库内某个节点用 wiki node delete", "未确认或只要看详情用 space get", }, Examples: []string{"dws wiki space delete --workspace --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "workspace", Property: "workspaceId"}, }, }, }) spaceDeleteCmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") spaceDeleteCmd.Flags().String("workspace-id", "", "") _ = spaceDeleteCmd.Flags().MarkHidden("workspace-id") RegisterCrossProductAliases(spaceDeleteCmd) spaceCmd.AddCommand(spaceCreateCmd, spaceGetCmd, spaceListCmd, spaceSearchCmd, spaceDeleteCmd) // ── member (知识库成员管理) ─────────────────────────────── memberCmd := &cobra.Command{ Use: "member", Short: "知识库成员管理", Long: `管理钉钉知识库的成员:添加成员、更新成员权限、查询成员列表、移除成员。`, RunE: groupRunE, } memberAddCmd := &cobra.Command{ Use: "add", Short: "添加知识库成员", Long: `为指定知识库添加一个或多个成员,并授予指定角色。 通过 --users 传入逗号分隔的 userId 列表,多个用户将被授予同一角色。 支持的角色 (--role)(必须大写): MANAGER 管理员,可读写、管理成员 EDITOR 编辑者,可查看、编辑、上传内容 DOWNLOADER 查看下载者,可查看并下载内容 READER 仅可查看者,仅可查看,不可下载 注意: - OWNER 角色不可通过此接口添加,知识库创建者默认为所有者。 - 操作者需具备知识库的 OWNER 或 MANAGER 权限。 - 单次请求最多 30 个成员,超出请分批调用。 支持通过 --workspace 传入知识库 ID 或知识库 URL,系统自动识别。 用户 uid 可通过「钉钉通讯录」相关命令检索,如: dws contact user search --keyword "姓名"`, Example: ` dws wiki member add --workspace --users uid1 --role READER dws wiki member add --workspace --users uid1,uid2,uid3 --role EDITOR dws wiki member add --workspace "https://alidocs.dingtalk.com/i/spaces/xxx/overview" --users uid1 --role MANAGER`, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } if err := validateRequiredFlags(cmd, "role"); err != nil { return err } role := normalizePermissionRole(mustGetFlag(cmd, "role")) if role == "OWNER" { return apperrors.NewValidation("OWNER 角色不可通过 wiki member add 添加") } userIds, err := collectUserIDs(cmd) if err != nil { return err } return callMCPTool("add_member", map[string]any{ "workspaceId": workspaceID, "roleId": role, "userIds": userIds, }) }, } DeclareLeafMetadata(memberAddCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "add_member", CanonicalPath: "wiki.add_member", CLIPath: "wiki member add", PrimaryCLIPath: "wiki member add", }, Description: "为指定知识库添加一个或多个成员,并授予指定角色", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "wiki", RPCName: "add_member"}, }, Selection: contract.SelectionSpec{ AgentSummary: "为指定知识库添加一个或多个成员,并授予指定角色", UseWhen: []string{"给知识库容器添加成员(USER + 角色);新员工入职开通整库访问时"}, AvoidWhen: []string{ "「我的文档」不支持容器成员——改用 drive/doc permission add 做节点授权", "单篇文档分享用节点 permission,不要用 member add", }, Examples: []string{ "dws wiki member add --workspace --users uid1 --role READER --format json", "dws wiki member add --workspace --users uid1,uid2 --role EDITOR --format json", }, }, Parameters: []contract.ParamDecl{ {Name: "role", Property: "roleId"}, {Name: "users", Property: "userIds"}, {Name: "workspace", Property: "workspaceId"}, }, }, }) memberAddCmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") memberAddCmd.Flags().String("users", "", "被添加的用户 userId 列表,逗号分隔 (必填,单次最多 30 个)") memberAddCmd.Flags().String("user", "", "") _ = memberAddCmd.Flags().MarkHidden("user") memberAddCmd.Flags().String("role", "", "权限角色: MANAGER / EDITOR / DOWNLOADER / READER (必填,大小写不敏感)") memberUpdateCmd := &cobra.Command{ Use: "update", Short: "更新知识库成员权限", Long: `更新指定知识库已有成员的角色。 支持的角色 (--role)(必须大写): MANAGER 管理员 EDITOR 编辑者 DOWNLOADER 查看下载者 READER 仅可查看者 注意: - OWNER 角色不可通过此接口变更。 - 同一成员在同一知识库只能拥有一个角色,变更后旧角色自动替换。 - 操作者需具备知识库的 OWNER 或 MANAGER 权限。 仅可更新已存在成员关系的成员,新增成员请使用 dws wiki member add。`, Example: ` dws wiki member update --workspace --users uid1 --role EDITOR dws wiki member update --workspace --users uid1,uid2 --role READER`, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } if err := validateRequiredFlags(cmd, "role"); err != nil { return err } userIds, err := collectUserIDs(cmd) if err != nil { return err } return callMCPTool("update_member", map[string]any{ "workspaceId": workspaceID, "roleId": normalizePermissionRole(mustGetFlag(cmd, "role")), "userIds": userIds, }) }, } DeclareLeafMetadata(memberUpdateCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "update_member", CanonicalPath: "wiki.update_member", CLIPath: "wiki member update", PrimaryCLIPath: "wiki member update", }, Description: "更新指定知识库已有成员的角色", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "wiki", RPCName: "update_member"}, }, Selection: contract.SelectionSpec{ AgentSummary: "更新指定知识库已有成员的角色", UseWhen: []string{"调整知识库成员角色(如升为 EDITOR/MANAGER)时"}, AvoidWhen: []string{"新加成员用 add;移除用 remove;OWNER 不可经此变更"}, Examples: []string{"dws wiki member update --workspace --users uid1 --role EDITOR --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "role", Property: "roleId"}, {Name: "users", Property: "userIds"}, {Name: "workspace", Property: "workspaceId"}, }, }, }) memberUpdateCmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") memberUpdateCmd.Flags().String("users", "", "被更新的用户 userId 列表,逗号分隔 (必填,单次最多 30 个)") memberUpdateCmd.Flags().String("user", "", "") _ = memberUpdateCmd.Flags().MarkHidden("user") memberUpdateCmd.Flags().String("role", "", "新权限角色: MANAGER / EDITOR / DOWNLOADER / READER (必填,大小写不敏感)") memberListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "查询知识库成员列表", Long: `查询指定知识库的成员列表,返回每位成员的 userId、姓名、角色等信息。 注意:底层不支持游标分页,--limit 仅控制单次返回的最大条数(最大 50)。 若结果被截断(出参 truncated=true),可通过 --filter-role 收窄查询范围; ORG 类型授权不会出现在查询结果中。`, Example: ` dws wiki member list --workspace dws wiki member list --workspace --limit 100 dws wiki member list --workspace --filter-role MANAGER,EDITOR`, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } toolArgs := map[string]any{ "workspaceId": workspaceID, } limit := 0 if cmd.Flags().Changed("limit") { limit, _ = cmd.Flags().GetInt("limit") } else if cmd.Flags().Changed("max-results") { limit, _ = cmd.Flags().GetInt("max-results") } if limit > 0 { if limit > 50 { return fmt.Errorf("--limit 不能超过 50;底层成员接口不提供游标续页") } toolArgs["maxResults"] = limit } if v := mustGetFlag(cmd, "filter-role"); v != "" { toolArgs["filterRoleIds"] = parseRoleList(v) } return callMCPTool("list_member", toolArgs) }, } DeclareLeafMetadata(memberListCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "list_member", CanonicalPath: "wiki.list_member", CLIPath: "wiki member list", PrimaryCLIPath: "wiki member list", }, Description: "查询指定知识库的成员列表,返回每位成员的 userId、姓名、角色等信息", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "wiki", RPCName: "list_member"}, }, Selection: contract.SelectionSpec{ AgentSummary: "查询指定知识库的成员列表,返回每位成员的 userId、姓名、角色等信息", UseWhen: []string{"查看知识库成员名单与角色时"}, AvoidWhen: []string{ "返回无 userId:要 update/remove 需另用 contact user search 反查", "增删改成员用 add/update/remove", }, Examples: []string{"dws wiki member list --workspace --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "filter-role", Property: "filterRoleIds"}, {Name: "limit", Property: "maxResults"}, {Name: "workspace", Property: "workspaceId"}, }, }, }) memberListCmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") memberListCmd.Flags().Int("limit", 30, "返回成员数上限,默认 30,最大 50;底层不支持游标续页") memberListCmd.Flags().Int("max-results", 0, "") _ = memberListCmd.Flags().MarkHidden("max-results") memberListCmd.Flags().String("filter-role", "", "按角色过滤(逗号分隔):OWNER / MANAGER / EDITOR / DOWNLOADER / READER") memberRemoveCmd := &cobra.Command{ Use: "remove", Short: "移除知识库成员", Long: `从指定知识库中移除一个或多个成员(仅支持 USER 类型)。 移除后相关用户将无法访问该知识库下的内容(除非通过节点级权限另行授权)。 注意: - OWNER 角色不可通过此接口移除。 - 操作者需具备知识库的 OWNER 或 MANAGER 权限。 - 单次请求最多 30 个成员,超出请分批调用。`, Example: ` dws wiki member remove --workspace --users uid1 dws wiki member remove --workspace --users uid1,uid2,uid3`, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } userIds, err := collectUserIDs(cmd) if err != nil { return err } return callMCPTool("remove_member", map[string]any{ "workspaceId": workspaceID, "userIds": userIds, }) }, } DeclareLeafMetadata(memberRemoveCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "remove_member", CanonicalPath: "wiki.remove_member", CLIPath: "wiki member remove", PrimaryCLIPath: "wiki member remove", }, Description: "从指定知识库中移除一个或多个成员(仅支持 USER 类型)", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "wiki", RPCName: "remove_member"}, }, Selection: contract.SelectionSpec{ AgentSummary: "从指定知识库中移除一个或多个成员(仅支持 USER 类型)", UseWhen: []string{"从知识库移除成员访问(离职/清理)时"}, AvoidWhen: []string{ "改角色用 update;节点级权限用 drive permission remove", "OWNER 不可移除此接口", }, Examples: []string{"dws wiki member remove --workspace --users uid1 --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "users", Property: "userIds"}, {Name: "workspace", Property: "workspaceId"}, }, }, }) memberRemoveCmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") memberRemoveCmd.Flags().String("users", "", "被移除的用户 userId 列表,逗号分隔 (必填,单次最多 30 个)") memberRemoveCmd.Flags().String("user", "", "") _ = memberRemoveCmd.Flags().MarkHidden("user") // member 子命令的 --workspace-id 隐藏别名(LLMs derive from API field "workspaceId") memberAliasCmds := []*cobra.Command{memberAddCmd, memberUpdateCmd, memberListCmd, memberRemoveCmd} for _, c := range memberAliasCmds { c.Flags().String("workspace-id", "", "") _ = c.Flags().MarkHidden("workspace-id") RegisterCrossProductAliases(c) } memberCmd.AddCommand(memberAddCmd, memberUpdateCmd, memberListCmd, memberRemoveCmd) root.AddCommand(spaceCmd, memberCmd) // ── node (知识库节点管理) ───────────────────────────────── // 对齐飞书 cli-lark wiki node 设计:内建 list/create/copy/move/delete, // 一跳直达 doc MCP server,不再经过 proxy chain。 nodeCmd := &cobra.Command{ Use: "node", Short: "知识库节点管理", Long: `管理钉钉知识库中的节点(文档/文件夹/表格等):列出、创建、复制、移动、删除。`, RunE: groupRunE, } nodeListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "列出知识库节点", Long: `列出指定知识库下的直接子节点(文档、文件夹、表格等)。 通过 --folder 指定父节点可列出子目录内容;不传 --folder 则列出知识库根目录。 支持分页,通过 --cursor 传入上次返回的 pageToken 获取下一页。`, Example: ` dws wiki node list --workspace dws wiki node list --workspace --folder dws wiki node list --workspace --limit 20 --cursor `, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } toolArgs := map[string]any{ "workspaceId": workspaceID, } if folder := docFolderFlag(cmd, "folder", "node", "parent-id"); folder != "" { if err := validateDocFolderID(folder); err != nil { return err } toolArgs["folderId"] = folder } if v, _ := cmd.Flags().GetInt("limit"); v > 0 { toolArgs["pageSize"] = v } if v := flagOrFallback(cmd, "cursor", "page-token"); v != "" { toolArgs["pageToken"] = v } return callMCPToolOnServer("doc", "list_nodes", toolArgs) }, } DeclareLeafMetadata(nodeListCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "list_nodes", CanonicalPath: "wiki.list_nodes", CLIPath: "wiki node list", PrimaryCLIPath: "wiki node list", }, Description: "列出指定知识库下的直接子节点(文档、文件夹、表格等)", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "doc", RPCName: "list_nodes"}, }, Selection: contract.SelectionSpec{ AgentSummary: "列出指定知识库下的直接子节点(文档、文件夹、表格等)", UseWhen: []string{"在已知 workspace 下浏览知识库根目录或某文件夹子节点时"}, AvoidWhen: []string{ "库内关键词搜索用 node search;全局搜用 drive search", "读文档内容拿到 nodeId 后用 doc read", }, Examples: []string{ "dws wiki node list --workspace --format json", "dws wiki node list --workspace --folder --format json", }, }, Parameters: []contract.ParamDecl{ {Name: "cursor", Property: "pageToken"}, {Name: "folder", Property: "folderId"}, {Name: "limit", Property: "pageSize"}, {Name: "workspace", Property: "workspaceId"}, }, }, }) nodeListCmd.Flags().String("workspace", "", "知识库 ID (必填)") nodeListCmd.Flags().String("folder", "", "父节点 nodeId (选填,不传则列出根目录)") nodeListCmd.Flags().Int("limit", 0, "每页数量 (默认 50,最大 50)") nodeListCmd.Flags().String("cursor", "", "分页游标") nodeCreateCmd := &cobra.Command{ Use: "create", Short: "在知识库中创建节点", Long: `在指定知识库中创建文档、文件夹或其他类型的节点。 通过 --type 指定节点类型(服务端支持以下值,asheet 不被支持): adoc 在线文档 (默认) axls 在线电子表格 able 多维表 appt 在线演示 adraw 白板/画板 amind 脑图 folder 文件夹 通过 --folder 指定父节点,不传则创建在知识库根目录。`, Example: ` dws wiki node create --workspace --name "新文档" dws wiki node create --workspace --name "方案目录" --type folder dws wiki node create --workspace --name "数据表" --type axls --folder `, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } if err := validateRequiredFlags(cmd, "name"); err != nil { return err } toolArgs := map[string]any{ "workspaceId": workspaceID, "name": mustGetFlag(cmd, "name"), } if v := mustGetFlag(cmd, "type"); v != "" { toolArgs["type"] = v } if folder := docFolderFlag(cmd, "folder", "parent-id"); folder != "" { if err := validateDocFolderID(folder); err != nil { return err } toolArgs["folderId"] = folder } return callMCPToolOnServer("doc", "create_file", toolArgs) }, } DeclareLeafMetadata(nodeCreateCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "create_file", CanonicalPath: "wiki.create_file", CLIPath: "wiki node create", PrimaryCLIPath: "wiki node create", }, Description: "在指定知识库中创建文档、文件夹或其他类型的节点", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "doc", RPCName: "create_file"}, }, Selection: contract.SelectionSpec{ AgentSummary: "在指定知识库中创建文档、文件夹或其他类型的节点", UseWhen: []string{"在知识库内创建空节点:adoc/axls/appt/adraw/amind/able/folder(必须 --workspace)时"}, AvoidWhen: []string{ "要带初始 Markdown 的文字文档可用 doc create", "钉盘普通文件夹用 drive mkdir(无需 workspace)", "type 不要用 asheet,在线表格用 axls", }, Examples: []string{ "dws wiki node create --workspace --name \"新文档\" --format json", "dws wiki node create --workspace --name \"方案目录\" --type folder --format json", }, }, Parameters: []contract.ParamDecl{ {Name: "folder", Property: "folderId"}, {Name: "workspace", Property: "workspaceId"}, }, }, }) nodeCreateCmd.Flags().String("workspace", "", "知识库 ID (必填)") nodeCreateCmd.Flags().String("name", "", "节点名称 (必填)") nodeCreateCmd.Flags().String("type", "adoc", "节点类型: adoc / axls / able / appt / adraw / amind / folder(asheet 不支持)") nodeCreateCmd.Flags().String("folder", "", "父节点 nodeId (选填,不传则在根目录创建)") nodeCopyCmd := &cobra.Command{ Use: "copy", Short: "复制知识库节点", Long: `将知识库中的节点复制到指定位置。 通过 --node 指定源节点,通过 --folder 指定目标文件夹。 不传 --folder 时复制到 --workspace 指定知识库的根目录。`, Example: ` dws wiki node copy --workspace --node dws wiki node copy --workspace --node --folder `, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } nodeID, err := mustFlagOrFallback(cmd, "node", "node-id", "doc-id", "file-id") if err != nil { return err } toolArgs := map[string]any{ "nodeId": nodeID, "workspaceId": workspaceID, } if folder := docFolderFlag(cmd, "folder", "parent-id", "parent-node-id", "parent-folder-id"); folder != "" { if err := validateDocFolderID(folder); err != nil { return err } toolArgs["targetFolderId"] = folder } return callMCPToolOnServer("doc", "copy_document", toolArgs) }, } DeclareLeafMetadata(nodeCopyCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "node_copy", CanonicalPath: "wiki.node_copy", CLIPath: "wiki node copy", PrimaryCLIPath: "wiki node copy", }, Description: "将知识库中的节点复制到指定位置", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "doc", RPCName: "copy_document"}, }, Selection: contract.SelectionSpec{ AgentSummary: "将知识库中的节点复制到指定位置", UseWhen: []string{"在知识库上下文内复制节点到同库某文件夹(需 --workspace + --node)时"}, AvoidWhen: []string{ "跨产品/默认文档空间复制优先 dws drive copy", "要移动不留副本用 node move", }, Examples: []string{"dws wiki node copy --workspace --node --folder --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "folder", Property: "targetFolderId"}, {Name: "node", Property: "nodeId"}, {Name: "workspace", Property: "workspaceId"}, }, }, }) nodeCopyCmd.Flags().String("workspace", "", "知识库 ID (必填)") nodeCopyCmd.Flags().String("node", "", "源节点 ID (必填)") nodeCopyCmd.Flags().String("folder", "", "目标文件夹 nodeId (选填)") nodeMoveCmd := &cobra.Command{ Use: "move", Short: "移动知识库节点", Long: `将知识库中的节点移动到指定位置。 通过 --node 指定源节点,通过 --folder 指定目标文件夹。 不传 --folder 时移动到 --workspace 指定知识库的根目录。 注意:跨知识库移动需要同时具备源和目标的相应权限。`, Example: ` dws wiki node move --workspace --node --folder dws wiki node move --workspace --node `, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } nodeID, err := mustFlagOrFallback(cmd, "node", "node-id", "doc-id", "file-id") if err != nil { return err } toolArgs := map[string]any{ "nodeId": nodeID, "workspaceId": workspaceID, } if folder := docFolderFlag(cmd, "folder", "parent-id", "parent-node-id", "parent-folder-id"); folder != "" { if err := validateDocFolderID(folder); err != nil { return err } toolArgs["targetFolderId"] = folder } return callMCPToolOnServer("doc", "move_document", toolArgs) }, } DeclareLeafMetadata(nodeMoveCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "node_move", CanonicalPath: "wiki.node_move", CLIPath: "wiki node move", PrimaryCLIPath: "wiki node move", }, Description: "将知识库中的节点移动到指定位置", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "doc", RPCName: "move_document"}, }, Selection: contract.SelectionSpec{ AgentSummary: "将知识库中的节点移动到指定位置", UseWhen: []string{"在知识库内移动节点到目标文件夹时"}, AvoidWhen: []string{"要保留副本用 node copy / drive copy"}, Examples: []string{"dws wiki node move --workspace --node --folder --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "folder", Property: "targetFolderId"}, {Name: "node", Property: "nodeId"}, {Name: "workspace", Property: "workspaceId"}, }, }, }) nodeMoveCmd.Flags().String("workspace", "", "知识库 ID (必填)") nodeMoveCmd.Flags().String("node", "", "源节点 ID (必填)") nodeMoveCmd.Flags().String("folder", "", "目标文件夹 nodeId (选填)") nodeDeleteCmd := &cobra.Command{ Use: "delete", Short: "删除知识库节点", Long: `将知识库中的节点移入回收站。 注意: 这是一个危险操作。执行前需要确认,或传入 --yes 跳过确认。 删除后节点会进入回收站,有保留期限可恢复。 权限要求: 对节点有"管理"权限。`, Example: ` dws wiki node delete --workspace --node dws wiki node delete --workspace --node --yes`, RunE: func(cmd *cobra.Command, args []string) error { if _, err := mustFlagOrFallback(cmd, "workspace", "workspace-id"); err != nil { return err } nodeID, err := mustFlagOrFallback(cmd, "node", "node-id", "doc-id", "file-id") if err != nil { return err } return callMCPToolOnServer("doc", "delete_document", map[string]any{ "nodeId": nodeID, }) }, } DeclareLeafMetadata(nodeDeleteCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "delete_document", CanonicalPath: "wiki.delete_document", CLIPath: "wiki node delete", PrimaryCLIPath: "wiki node delete", }, Description: "将知识库中的节点移入回收站", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "doc", RPCName: "delete_document"}, }, Selection: contract.SelectionSpec{ AgentSummary: "将知识库中的节点移入回收站", UseWhen: []string{"用户确认后删除知识库内节点(移入回收站;需 --workspace 校验权限)时"}, AvoidWhen: []string{ "删整个知识库用 space delete", "未确认或 node 不明时不要删", }, Examples: []string{"dws wiki node delete --workspace --node --format json"}, }, Parameters: []contract.ParamDecl{ {Name: "node", Property: "nodeId"}, }, }, }) nodeDeleteCmd.Flags().String("workspace", "", "知识库 ID (必填,用于权限校验)") nodeDeleteCmd.Flags().String("node", "", "节点 ID (必填)") nodeSearchCmd := &cobra.Command{ Use: "search", Short: "在知识库中搜索节点", Long: `在指定知识库内搜索文档/文件夹/表格等节点。 通过 --workspace 限定搜索范围到某个知识库,通过 --query 指定搜索关键词。 支持按文件扩展名过滤(--extensions),如 adoc、asheet、pdf 等。`, Example: ` dws wiki node search --workspace --query "产品方案" dws wiki node search --workspace --query "设计" --extensions adoc,asheet dws wiki node search --workspace --query "合同" --limit 20`, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } query := flagOrFallback(cmd, "query", "keyword") if query == "" { return fmt.Errorf("flag --query is required") } toolArgs := map[string]any{ "keyword": query, "workspaceIds": []string{workspaceID}, } if v, _ := cmd.Flags().GetStringSlice("extensions"); len(v) > 0 { toolArgs["extensions"] = v } if v, _ := cmd.Flags().GetInt("limit"); v > 0 { toolArgs["pageSize"] = v } if v := flagOrFallback(cmd, "cursor", "page-token"); v != "" { toolArgs["pageToken"] = v } return callMCPToolOnServer("doc", "search_documents", toolArgs) }, } DeclareLeafMetadata(nodeSearchCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "node_search", CanonicalPath: "wiki.node_search", CLIPath: "wiki node search", PrimaryCLIPath: "wiki node search", }, Description: "在指定知识库内搜索文档/文件夹/表格等节点", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "doc", RPCName: "search_documents"}, }, Selection: contract.SelectionSpec{ AgentSummary: "在指定知识库内搜索文档/文件夹/表格等节点", UseWhen: []string{"在指定知识库内按关键词搜节点(可 --extensions)时"}, AvoidWhen: []string{"全局/钉盘搜索用 drive search;浏览目录用 node list"}, Examples: []string{ "dws wiki node search --workspace --query \"方案\" --format json", "dws wiki node search --workspace --query \"周报\" --extensions adoc --format json", }, }, Parameters: []contract.ParamDecl{ {Name: "cursor", Property: "pageToken"}, {Name: "limit", Property: "pageSize"}, {Name: "query", Property: "keyword"}, {Name: "workspace", Property: "workspaceIds"}, }, }, }) nodeSearchCmd.Flags().String("workspace", "", "知识库 ID (必填)") nodeSearchCmd.Flags().String("query", "", "搜索关键词 (必填)") nodeSearchCmd.Flags().String("keyword", "", "--query 的别名") _ = nodeSearchCmd.Flags().MarkHidden("keyword") nodeSearchCmd.Flags().StringSlice("extensions", nil, "按文件扩展名过滤 (如 adoc,asheet,pdf)") nodeSearchCmd.Flags().Int("limit", 0, "每页数量 (默认 10,最大 30)") nodeSearchCmd.Flags().String("cursor", "", "分页游标") // node 子命令的 hidden aliases nodeAliasCmds := []*cobra.Command{nodeListCmd, nodeCreateCmd, nodeCopyCmd, nodeMoveCmd, nodeDeleteCmd, nodeSearchCmd} for _, c := range nodeAliasCmds { c.Flags().String("workspace-id", "", "") _ = c.Flags().MarkHidden("workspace-id") RegisterCrossProductAliases(c) } nodeCmd.AddCommand(nodeListCmd, nodeCreateCmd, nodeCopyCmd, nodeMoveCmd, nodeDeleteCmd, nodeSearchCmd) root.AddCommand(nodeCmd) // ── feed (知识库动态查询) ───────────────────────────────── feedCmd := &cobra.Command{ Use: "feed", Short: "知识库动态查询", Long: `查询知识库的动态:谁在什么时间更新/上传/评论了哪些文档。`, RunE: groupRunE, } feedListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "查询知识库动态列表", Long: `查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息。 通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。 支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。 使用 --exclude-file 可排除普通文件、媒体文件、文件夹及 Office 文件动态,仅保留在线文档操作(创建/更新/评论/点赞等)。 当用户要求"只看文档操作""排除文件上传"等意图时,必须使用此 flag, 禁止在客户端自行过滤。`, Example: ` dws wiki feed list --workspace dws wiki feed list --workspace --limit 10 dws wiki feed list --workspace --exclude-file dws wiki feed list --workspace --limit 10 --exclude-file dws wiki feed list --workspace --cursor `, RunE: func(cmd *cobra.Command, args []string) error { workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id") if err != nil { return err } toolArgs := map[string]any{ "workspaceId": workspaceID, } if v, _ := cmd.Flags().GetInt("limit"); v > 0 { toolArgs["maxResults"] = v } if v := flagOrFallback(cmd, "cursor", "page-token"); v != "" { toolArgs["nextToken"] = v } if cmd.Flags().Changed("exclude-file") { v, _ := cmd.Flags().GetBool("exclude-file") toolArgs["excludeFile"] = v } // 调用后对 feeds[].time 毫秒时间戳做本地化格式化, // 使 Agent 直接看到可读时间,无需自行转换 text, err := callMCPToolReturnText(context.Background(), "list_workspace_feeds", toolArgs) if err != nil { return err } // 保持与共享 dispatcher (callMCPToolInternalOpts) 一致的输出契约: // 仅 --format json 做时间/标签增强,raw、table 及其他格式输出原始 MCP 文本 if deps.Caller.Format() != "json" { deps.Out.PrintRaw(text) return nil } return deps.Out.PrintJSON(formatFeedTime(text)) }, } DeclareLeafMetadata(feedListCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "wiki", Name: "list_workspace_feeds", CanonicalPath: "wiki.list_workspace_feeds", CLIPath: "wiki feed list", PrimaryCLIPath: "wiki feed list", }, Description: "查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "wiki", RPCName: "list_workspace_feeds"}, }, Selection: contract.SelectionSpec{ AgentSummary: "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档", UseWhen: []string{ "用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时", "需要巡检知识库变更并按时间线汇总成动态摘要时", }, AvoidWhen: []string{ "要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search", "要读某篇文档正文用 doc read;跨库全局找文件用 drive search", }, Examples: []string{ "dws wiki feed list --workspace --format json", "dws wiki feed list --workspace --limit 10 --format json", }, }, Parameters: []contract.ParamDecl{ {Name: "cursor", Property: "nextToken"}, {Name: "exclude-file", Property: "excludeFile"}, {Name: "limit", Property: "maxResults"}, {Name: "workspace", Property: "workspaceId"}, }, }, }) feedListCmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") feedListCmd.Flags().Int("limit", 0, "每页数量 (默认 10,最大 20)。用户未明确要求条数时禁止加此 flag,让服务端走默认 10") feedListCmd.Flags().String("cursor", "", "分页游标 (首页留空)") feedListCmd.Flags().Bool("exclude-file", false, "排除普通文件、媒体文件、文件夹及 Office 文件动态,仅保留在线文档操作(创建/更新/评论/点赞)。用户要求排除文件/只看文档操作时必须使用此 flag,禁止客户端过滤") feedListCmd.Flags().String("workspace-id", "", "") _ = feedListCmd.Flags().MarkHidden("workspace-id") RegisterCrossProductAliases(feedListCmd) feedCmd.AddCommand(feedListCmd) root.AddCommand(feedCmd) // ── [PROXY] wiki create/get/list/search → wiki space create/get/list/search ── // Agent 常省略 "space" 直接输入 dws wiki list,透明转发到 wiki space 对应命令 root.AddCommand( proxySubCmd("create", "wiki", "space create", nil), // [PROXY] wiki create → wiki space create proxySubCmd("get", "wiki", "space get", nil), // [PROXY] wiki get → wiki space get proxySubCmd("list", "wiki", "space list", nil), // [PROXY] wiki list → wiki space list proxySubCmd("search", "wiki", "space search", nil), // [PROXY] wiki search → wiki space search proxySubCmd("delete", "wiki", "space delete", nil), // [PROXY] wiki delete → wiki space delete ) // ── [PROXY] wiki file/doc * → doc * (兼容旧路径) ── fileGroup := &cobra.Command{Use: "file", Hidden: true, RunE: groupRunE} fileGroup.AddCommand( proxySubCmd("list", "doc", "list", nil), // [PROXY] wiki file list → doc list proxySubCmd("search", "doc", "search", map[string]string{"workspace": "workspace-ids"}), // [PROXY] wiki file search → doc search ) root.AddCommand(fileGroup) docGroup := &cobra.Command{Use: "doc", Hidden: true, RunE: groupRunE} docGroup.AddCommand( proxySubCmd("list", "doc", "list", nil), // [PROXY] wiki doc list → doc list proxySubCmd("read", "doc", "read", nil), // [PROXY] wiki doc read → doc read proxySubCmd("search", "doc", "search", map[string]string{"workspace": "workspace-ids"}), // [PROXY] wiki doc search → doc search ) root.AddCommand(docGroup) return root } // beijingLoc 用于将时间戳格式化为北京时间(UTC+8) var beijingLoc = time.FixedZone("CST", 8*3600) // formatFeedTime 将知识库动态 feeds[].time 毫秒时间戳转为可读时间字符串, // 写入 timeFormatted 字段,同时保留原始 time 毫秒时间戳不变(避免破坏已有脚本)。 // 解析失败时原样返回,不阻断输出。 func formatFeedTime(raw string) any { var result map[string]any if err := json.Unmarshal([]byte(raw), &result); err != nil { return raw } feeds, ok := result["feeds"].([]any) if !ok { return result } for _, f := range feeds { item, ok := f.(map[string]any) if !ok { continue } ms, ok := item["time"].(float64) if !ok || ms <= 0 { continue } // 保留原始 time 毫秒时间戳,新增 timeFormatted 可读字段 item["timeFormatted"] = time.UnixMilli(int64(ms)).In(beijingLoc).Format("2006-01-02 15:04") enrichFeedFields(item) } return result } // enrichFeedFields 为单条 feed 补充可读字段(typeLabel), func enrichFeedFields(item map[string]any) { // 将 type 数字映射为可读标签,使 Agent 无需查表即可直接展示 if typeNum, ok := item["type"].(float64); ok { if label, known := feedTypeLabels[int(typeNum)]; known { item["typeLabel"] = label } } } // feedTypeLabels 将知识库动态 type 数字映射为可读中文标签, // 与 lippi-combo OpenFeedItemDTO.type 枚举对齐。 var feedTypeLabels = map[int]string{ 0: "创建文档", 1: "更新文档", 2: "评论文档", 3: "点赞文档", 4: "加入团队空间", 5: "表格选区数据变更", 6: "更新 office 文件", 7: "上传普通文件", 8: "上传媒体文件", 9: "上传文件夹", 10: "上传文件夹 V2", 11: "加入团队", 12: "创建知识库", }