// Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 package drive import ( "fmt" "strings" "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" ) var Inspect = shortcut.Shortcut{ Service: "drive", Command: "+inspect", Product: "drive", Description: "聚合检查节点元数据及可选统计、公开状态和封面", Intent: "已知节点 ID,需要一次确认名称、类型、路径,并可附带统计、公开状态或封面时使用。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: driveContract( "+inspect", "聚合检查节点元数据及可选统计、公开状态和封面", "已知节点 ID,需要一次确认名称、类型、路径,并可附带统计、公开状态或封面时使用。", []string{"读取在线文档正文使用 doc +fetch;浏览目录使用 drive +list"}, []string{`dws drive +inspect --node `, `dws drive +inspect --node --include-stats --include-publish`}, driveObjectResult("Drive 节点聚合检查结果"), nil, contract.ParamDecl{Name: "node", Property: "fileId"}, ), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "节点 ID", Required: true}, {Name: "space-id", Type: shortcut.FlagString, Desc: "钉盘空间 ID"}, {Name: "include-stats", Type: shortcut.FlagBool, Desc: "附带阅读、编辑、下载等统计"}, {Name: "include-publish", Type: shortcut.FlagBool, Desc: "附带互联网公开状态"}, {Name: "include-cover", Type: shortcut.FlagBool, Desc: "附带封面或缩略图地址"}, }, Tips: []string{`dws drive +inspect --node `, `dws drive +inspect --node --include-stats --include-publish`}, Execute: func(rt *shortcut.RuntimeContext) error { node := rt.Str("node") params := map[string]any{"fileId": node} if rt.Str("space-id") != "" { params["spaceId"] = rt.Str("space-id") } data, err := rt.CallMCPData("drive", "get_file_info", params) if err != nil { return err } info, err := requireDriveObject(data, "drive/get_file_info") if err != nil { return err } result := map[string]any{"file": info} steps := []map[string]any{{"tool": "get_file_info", "status": "success"}} reads := []struct { flag, key, tool string params map[string]any }{ {"include-stats", "stats", "get_node_stats", map[string]any{"nodeId": node}}, {"include-publish", "publish", "get_file_publish_status", map[string]any{"fileId": node}}, {"include-cover", "cover", "get_cover", map[string]any{"nodeId": node}}, } failures := []map[string]any{} for _, read := range reads { if !rt.Bool(read.flag) { continue } value, callErr := rt.CallMCPReadData("drive", read.tool, read.params) if callErr == nil { value, callErr = requireDriveObject(value, "drive/"+read.tool) } if callErr != nil { steps = append(steps, map[string]any{"tool": read.tool, "status": "failed"}) failures = append(failures, map[string]any{"tool": read.tool, "error": callErr.Error()}) continue } result[read.key] = value steps = append(steps, map[string]any{"tool": read.tool, "status": "success"}) } if len(failures) > 0 { return apperrors.NewAPI("Drive 聚合检查只完成了部分读取", apperrors.WithOperation("drive.inspect"), apperrors.WithReason("drive_inspect_partial"), apperrors.WithFailureStage("optional_reads"), apperrors.WithExecutionStarted(false), apperrors.WithRetryable(true), apperrors.WithDetails(map[string]any{"status": "partial_success", "complete": false, "data": result, "steps": steps, "failures": failures}), ) } return rt.Output(map[string]any{"status": "success", "complete": true, "data": result, "steps": steps}) }, } var CreateFolder = shortcut.Shortcut{ Service: "drive", Command: "+create-folder", Product: "drive", Description: "创建钉盘文件夹并读回验证", Intent: "在钉盘中创建普通文件夹并需要拿到经过读回验证的 fileId 时使用。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown"}, Contract: driveContract( "+create-folder", "创建钉盘文件夹并读回验证", "在钉盘中创建普通文件夹并需要拿到经过读回验证的 fileId 时使用。", []string{"知识库目录使用 wiki node create;创建在线文档使用 doc +create"}, []string{`dws drive +create-folder --name "项目资料"`, `dws drive +create-folder --name "子目录" --folder `}, driveObjectResult("创建并验证后的文件夹"), nil, contract.ParamDecl{Name: "folder", Property: "parentId"}, ), Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "文件夹名称", Required: true}, {Name: "space-id", Type: shortcut.FlagString, Desc: "钉盘空间 ID"}, {Name: "folder", Type: shortcut.FlagString, Desc: "父文件夹 ID"}, }, Tips: []string{`dws drive +create-folder --name "项目资料"`, `dws drive +create-folder --name "子目录" --folder `}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"name": rt.Str("name")} if rt.Str("space-id") != "" { params["spaceId"] = rt.Str("space-id") } if rt.Str("folder") != "" { params["parentId"] = rt.Str("folder") } created, err := rt.CallMCPWriteDataStrict("drive", "create_folder", params) if err != nil { return err } created, err = requireDriveWrite(created, "drive/create_folder") if err != nil { return err } nodeID := nestedString(created, "fileId", "dentryUuid", "nodeId", "id") if nodeID == "" { return driveResponseError("drive/create_folder", "missing_created_id", "创建响应没有文件夹 fileId;远端效果未知") } verified, err := rt.CallMCPData("drive", "get_file_info", map[string]any{"fileId": nodeID}) if err != nil { return err } verified, err = requireDriveObject(verified, "drive/get_file_info") if err != nil { return err } if name := firstString(verified, "name", "fileName"); name != rt.Str("name") { return driveResponseError("drive/create_folder", "readback_mismatch", fmt.Sprintf("创建后读回名称 %q 与请求 %q 不一致", name, rt.Str("name"))) } return rt.Output(map[string]any{"success": true, "nodeId": nodeID, "folder": verified}) }, } var CreateShortcut = shortcut.Shortcut{ Service: "drive", Command: "+create-shortcut", Product: "drive", Description: "为已有节点创建快捷方式并验证新节点", Intent: "给已有文件或文档创建快捷入口,并需要区分 shortcut 与独立副本时使用。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent"}, Contract: driveContract( "+create-shortcut", "为已有节点创建快捷方式并验证新节点", "给已有文件或文档创建快捷入口,并需要区分 shortcut 与独立副本时使用。", []string{"需要独立副本使用 drive +copy;需要迁移原节点使用 drive +move"}, []string{`dws drive +create-shortcut --node --folder `}, driveObjectResult("创建并验证后的快捷方式节点"), nil, contract.ParamDecl{Name: "node", Property: "nodeId"}, contract.ParamDecl{Name: "folder", Property: "targetFolderId"}, contract.ParamDecl{Name: "workspace", Property: "workspaceId"}, ), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "源节点 ID", Required: true}, {Name: "folder", Type: shortcut.FlagString, Desc: "目标文件夹 ID"}, {Name: "workspace", Type: shortcut.FlagString, Desc: "目标知识库 ID"}, }, Tips: []string{`dws drive +create-shortcut --node --folder `}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"nodeId": rt.Str("node")} if rt.Str("folder") != "" { params["targetFolderId"] = rt.Str("folder") } if rt.Str("workspace") != "" { params["workspaceId"] = rt.Str("workspace") } created, err := rt.CallMCPWriteDataStrict("drive", "create_shortcut", params) if err != nil { return err } created, err = requireDriveWrite(created, "drive/create_shortcut") if err != nil { return err } nodeID := nestedString(created, "fileId", "dentryUuid", "nodeId", "id") if nodeID == "" { return driveResponseError("drive/create_shortcut", "missing_created_id", "创建快捷方式未返回新节点 ID") } verified, err := rt.CallMCPData("drive", "get_file_info", map[string]any{"fileId": nodeID}) if err != nil { return err } verified, err = requireDriveObject(verified, "drive/get_file_info") if err != nil { return err } return rt.Output(map[string]any{"success": true, "nodeId": nodeID, "shortcut": verified}) }, } var Rename = shortcut.Shortcut{ Service: "drive", Command: "+rename", Product: "doc", Description: "重命名文件或文件夹并读回验证", Intent: "改变已有节点名称并要求验证最终名称时使用。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"}, Contract: driveContract( "+rename", "重命名文件或文件夹并读回验证", "改变已有节点名称并要求验证最终名称时使用。", []string{"改变位置使用 drive +move;替换文件内容使用 drive +upload --node"}, []string{`dws drive +rename --node --name "新名称"`}, driveObjectResult("重命名后的节点元数据"), nil, contract.ParamDecl{Name: "node", Property: "nodeId"}, contract.ParamDecl{Name: "name", Property: "newName"}, ), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "节点 ID", Required: true}, {Name: "name", Type: shortcut.FlagString, Desc: "新名称", Required: true}, }, Tips: []string{`dws drive +rename --node --name "新名称"`}, Execute: func(rt *shortcut.RuntimeContext) error { preflight, err := rt.CallMCPData("drive", "get_file_info", map[string]any{"fileId": rt.Str("node")}) if err != nil { return err } preflight, err = requireDriveObject(preflight, "drive/get_file_info") if err != nil { return err } requestName, expectedNames := normalizedDriveRename(rt.Str("name"), preflight) written, err := rt.CallMCPWriteDataStrict("doc", "rename_document", map[string]any{"nodeId": rt.Str("node"), "newName": requestName}) if err != nil { return err } if _, err := requireDriveWrite(written, "doc/rename_document"); err != nil { return err } verified, err := rt.CallMCPData("drive", "get_file_info", map[string]any{"fileId": rt.Str("node")}) if err != nil { return err } verified, err = requireDriveObject(verified, "drive/get_file_info") if err != nil { return err } name := firstString(verified, "name", "fileName") if name == "" || !expectedNames[name] { return driveResponseError("doc/rename_document", "readback_mismatch", fmt.Sprintf("重命名读回名称 %q 与请求 %q 不一致", name, rt.Str("name"))) } return rt.Output(map[string]any{"success": true, "nodeId": rt.Str("node"), "file": verified}) }, } func normalizedDriveRename(name string, info map[string]any) (string, map[string]bool) { name = strings.TrimSpace(name) expected := map[string]bool{name: true} nodeType := strings.ToLower(firstString(info, "type", "nodeType", "fileType")) if nodeType == "folder" || nodeType == "dir" || nodeType == "directory" { return name, expected } extension := strings.TrimLeft(strings.ToLower(firstString(info, "extension", "fileExtension", "ext")), ".") if extension == "" { return name, expected } suffix := "." + extension if len(name) > len(suffix) && strings.EqualFold(name[len(name)-len(suffix):], suffix) { return name[:len(name)-len(suffix)], expected } expected[name+suffix] = true return name, expected } var Delete = shortcut.Shortcut{ Service: "drive", Command: "+delete", Product: "doc", Description: "将已确认节点移入回收站", Intent: "用户明确要求删除已确认钉盘节点,并理解它会进入回收站时使用。", Risk: shortcut.RiskHighWrite, Safety: contract.SafetySpec{Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown"}, Contract: driveContract( "+delete", "将已确认节点移入回收站", "用户明确要求删除已确认钉盘节点,并理解它会进入回收站时使用。", []string{"只是调整位置使用 drive +move;目标不明确时先 drive +inspect"}, []string{`dws drive +delete --node `}, driveObjectResult("删除到回收站的终态证据"), nil, contract.ParamDecl{Name: "node", Property: "nodeId"}, ), Flags: []shortcut.Flag{{Name: "node", Type: shortcut.FlagString, Desc: "节点 ID", Required: true}}, Tips: []string{`dws drive +delete --node `}, Execute: func(rt *shortcut.RuntimeContext) error { written, err := rt.CallMCPWriteDataStrict("doc", "delete_document", map[string]any{"nodeId": rt.Str("node")}) if err != nil { return err } written, err = requireDriveWrite(written, "doc/delete_document") if err != nil { return err } return rt.Output(map[string]any{"success": true, "nodeId": rt.Str("node"), "result": written}) }, } var Stats = driveReadObjectShortcut( "+stats", "读取节点访问和协作统计", "get_node_stats", "nodeId", "用户要查看指定节点阅读、编辑、评论、点赞、预览或下载统计时使用。", []string{"只要文件名称和类型使用 drive +inspect"}, ) var Cover = driveReadObjectShortcut( "+cover", "读取节点封面或缩略图地址", "get_cover", "nodeId", "用户需要节点封面、首图或缩略图 URL 时使用。", []string{"这是封面资源,不等于 Lark 服务端多格式预览转换"}, ) func driveReadObjectShortcut(command, description, tool, property, useWhen string, avoidWhen []string) shortcut.Shortcut { return shortcut.Shortcut{ Service: "drive", Command: command, Product: "drive", Description: description, Intent: useWhen, Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: driveContract(command, description, useWhen, avoidWhen, []string{fmt.Sprintf("dws drive %s --node ", command)}, driveObjectResult(description), nil, contract.ParamDecl{Name: "node", Property: property}), Flags: []shortcut.Flag{{Name: "node", Type: shortcut.FlagString, Desc: "节点 ID", Required: true}}, Tips: []string{fmt.Sprintf("dws drive %s --node ", command)}, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData("drive", tool, map[string]any{property: rt.Str("node")}) if err != nil { return err } data, err = requireDriveObject(data, "drive/"+tool) if err != nil { return err } return rt.Output(data) }, } }