From 5737a672ad6a8c6765807c1d5ab44d8039109d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:42:03 +0800 Subject: [PATCH 1/3] fix(cli): keep -y shorthand on todo delete & skill setup (#370) Both commands registered a local --yes flag without the -y shorthand, shadowing the global persistent -y/--yes so 'dws todo task delete -y' and 'dws skill setup -y' failed with 'unknown shorthand flag: y'. Use BoolP("yes","y",...) to match the working drive delete pattern. --- internal/app/skill_setup.go | 2 +- internal/helpers/todo.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/skill_setup.go b/internal/app/skill_setup.go index 4a43f195..49849004 100644 --- a/internal/app/skill_setup.go +++ b/internal/app/skill_setup.go @@ -69,7 +69,7 @@ multi 模式支持按产品挑选: cmd.Flags().String("mode", "", "skill 模式:mono | multi(不指定则交互询问)") cmd.Flags().String("target", "all", "目标 Agent:all | "+supportedTargets()) cmd.Flags().String("source", "", "skill 源目录(默认自动查找二进制旁边或当前目录)") - cmd.Flags().Bool("yes", false, "跳过所有确认提示") + cmd.Flags().BoolP("yes", "y", false, "跳过所有确认提示") cmd.Flags().StringSliceP("skill", "s", nil, "multi 模式:仅安装指定子 skill(可重复,接受短名 aitable 或全名 dingtalk-aitable)") cmd.Flags().StringSliceP("exclude", "x", nil, "multi 模式:从全装中剔除指定子 skill(可重复,与 --skill 互斥)") return cmd diff --git a/internal/helpers/todo.go b/internal/helpers/todo.go index 7f2b0bc1..7aea3bba 100644 --- a/internal/helpers/todo.go +++ b/internal/helpers/todo.go @@ -466,7 +466,7 @@ func newTodoTaskDeleteCommand(runner executor.Runner) *cobra.Command { preferLegacyLeaf(cmd) cmd.Flags().String("task-id", "", i18n.T("待办任务 ID (必填)")) - cmd.Flags().Bool("yes", false, i18n.T("跳过确认直接删除")) + cmd.Flags().BoolP("yes", "y", false, i18n.T("跳过确认直接删除")) return cmd } From c8d0e98d2008edc757cca81064bd205609555d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:42:06 +0800 Subject: [PATCH 2/3] fix(cli): unshadow global -f/--format and emit JSON for action-less group nodes (#421, #422) #421: aitable export data registered a local --format flag with the same long name as the global persistent output flag --format/-f, dropping the -f shorthand on that command. Rename the export flag to --export-format (aligns with doc.go); the MCP param key stays "format". #422: action-less group nodes printed human usage text even under -f json, breaking JSON-only consumers. NewGroupCommand now returns a structured validation error (with available subcommands) when the user explicitly requests json/ndjson, while a bare invocation still shows help. --- internal/cobracmd/tree.go | 50 +++++++++++++++++++++++ internal/helpers/aitable_commands_test.go | 6 +-- internal/helpers/aitable_export_import.go | 18 ++++---- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/internal/cobracmd/tree.go b/internal/cobracmd/tree.go index 60813c63..3543fbf3 100644 --- a/internal/cobracmd/tree.go +++ b/internal/cobracmd/tree.go @@ -16,10 +16,13 @@ package cobracmd import ( + "fmt" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" + + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" ) // ChildByName returns the child command with the given name, or nil. @@ -42,6 +45,12 @@ func FlagChanged(cmd *cobra.Command, name string) bool { } // NewGroupCommand creates a non-leaf parent command that shows help when invoked. +// +// An action-less group node has no tool of its own; invoking it directly just +// lists its subcommands. For humans that means printed usage text. But when the +// caller explicitly requests JSON output (-f json), usage text breaks the +// "JSON-only" contract that agents/MCP consumers rely on, so we instead return a +// structured validation error naming the available subcommands. See issue #422. func NewGroupCommand(use, short string) *cobra.Command { return &cobra.Command{ Use: use, @@ -50,11 +59,52 @@ func NewGroupCommand(use, short string) *cobra.Command { TraverseChildren: true, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + if explicitJSONFormat(cmd) { + subs := visibleSubcommandNames(cmd) + msg := fmt.Sprintf("%q requires a subcommand", cmd.CommandPath()) + if len(subs) > 0 { + msg = fmt.Sprintf("%s; available: %s", msg, strings.Join(subs, ", ")) + } + return apperrors.NewValidation(msg) + } return cmd.Help() }, } } +// explicitJSONFormat reports whether the user explicitly selected a JSON-family +// output format via -f/--format. It checks Changed so that a bare group +// invocation (relying on the default format) still gets human-readable help. +func explicitJSONFormat(cmd *cobra.Command) bool { + pf := cmd.Root().PersistentFlags() + if !pf.Changed("format") { + return false + } + f, err := pf.GetString("format") + if err != nil { + return false + } + switch strings.ToLower(strings.TrimSpace(f)) { + case "json", "ndjson": + return true + default: + return false + } +} + +// visibleSubcommandNames returns the names of the command's non-hidden, +// non-help subcommands, for surfacing in the action-less JSON error. +func visibleSubcommandNames(cmd *cobra.Command) []string { + var names []string + for _, child := range cmd.Commands() { + if child.Hidden || child.Name() == "help" || !child.IsAvailableCommand() { + continue + } + names = append(names, child.Name()) + } + return names +} + // NewHiddenGroupCommand creates a hidden non-leaf parent command. func NewHiddenGroupCommand(use, short string) *cobra.Command { cmd := NewGroupCommand(use, short) diff --git a/internal/helpers/aitable_commands_test.go b/internal/helpers/aitable_commands_test.go index b363c354..3d1d324e 100644 --- a/internal/helpers/aitable_commands_test.go +++ b/internal/helpers/aitable_commands_test.go @@ -185,7 +185,7 @@ func TestAitableExportDataDryRunUsesWukongFormatFlags(t *testing.T) { "--base-id", "BASE_001", "--scope", "table", "--table-id", "TBL_001", - "--format", "excel", + "--export-format", "excel", "--timeout-ms", "1000", }) @@ -234,7 +234,7 @@ func TestAitableExportDataRequiresFormatWhenCreatingTask(t *testing.T) { }) if err := cmd.Execute(); err == nil { - t.Fatal("Execute() error = nil, want missing --format validation") + t.Fatal("Execute() error = nil, want missing --export-format validation") } if runner.last.Tool != "" { t.Fatalf("tool = %q, want no invocation", runner.last.Tool) @@ -304,7 +304,7 @@ func TestAitableExportDataLiveCallsExportDataOnce(t *testing.T) { "--base-id", "BASE_001", "--scope", "table", "--table-id", "TBL_001", - "--format", "excel", + "--export-format", "excel", "--timeout-ms", "1000", }) diff --git a/internal/helpers/aitable_export_import.go b/internal/helpers/aitable_export_import.go index 38ce1a3e..5d71c1ab 100644 --- a/internal/helpers/aitable_export_import.go +++ b/internal/helpers/aitable_export_import.go @@ -43,15 +43,15 @@ func newAitableExportDataCommand(runner executor.Runner) *cobra.Command { Use: "data", Short: i18n.T("导出数据"), Long: i18n.T(`导出 AI 表格数据的统一入口。 -不传 --task-id 时,根据 --scope / --format 创建新的导出任务,并同步等待结果; +不传 --task-id 时,根据 --scope / --export-format 创建新的导出任务,并同步等待结果; 若在等待窗口内完成,则直接返回 downloadUrl 和 fileName。 传入 --task-id 时,继续等待该任务,不会重新创建。 scope 可选值:all(整个 Base)、table(指定数据表)、view(指定视图)。 -format 可选值:excel、attachment、excel_and_attachment、excel_with_inline_images。`), - Example: ` dws aitable export data --base-id BASE_ID --scope all --format excel - dws aitable export data --base-id BASE_ID --scope table --table-id TABLE_ID --format excel - dws aitable export data --base-id BASE_ID --scope view --table-id TABLE_ID --view-id VIEW_ID --format excel +export-format 可选值:excel、attachment、excel_and_attachment、excel_with_inline_images。`), + Example: ` dws aitable export data --base-id BASE_ID --scope all --export-format excel + dws aitable export data --base-id BASE_ID --scope table --table-id TABLE_ID --export-format excel + dws aitable export data --base-id BASE_ID --scope view --table-id TABLE_ID --view-id VIEW_ID --export-format excel dws aitable export data --base-id BASE_ID --task-id TASK_ID # 查询 baseId: dws aitable base list`, Args: cobra.NoArgs, @@ -64,8 +64,8 @@ format 可选值:excel、attachment、excel_and_attachment、excel_with_inline cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名") cmd.Flags().String("scope", "", i18n.T("导出范围:all(整个 Base)、table(指定数据表)、view(指定视图)")) - cmd.Flags().String("format", "", i18n.T("导出格式:excel、attachment、excel_and_attachment、excel_with_inline_images")) - cmd.Flags().String("task-id", "", i18n.T("已有导出任务 ID,传入后继续等待(忽略 scope/format/table-id/view-id)")) + cmd.Flags().String("export-format", "", i18n.T("导出格式:excel、attachment、excel_and_attachment、excel_with_inline_images")) + cmd.Flags().String("task-id", "", i18n.T("已有导出任务 ID,传入后继续等待(忽略 scope/export-format/table-id/view-id)")) cmd.Flags().String("table-id", "", i18n.T("Table ID,scope=table 或 scope=view 时必填")) cmd.Flags().String("view-id", "", i18n.T("View ID,scope=view 时必填")) cmd.Flags().Int("timeout-ms", 0, i18n.T("单次等待超时(毫秒),默认 30000,最大 30000")) @@ -79,7 +79,7 @@ func runAitableExportData(cmd *cobra.Command, runner executor.Runner) error { } taskID, _ := cmd.Flags().GetString("task-id") scope, _ := cmd.Flags().GetString("scope") - format, _ := cmd.Flags().GetString("format") + format, _ := cmd.Flags().GetString("export-format") tableID, _ := cmd.Flags().GetString("table-id") viewID, _ := cmd.Flags().GetString("view-id") timeoutMS, _ := cmd.Flags().GetInt("timeout-ms") @@ -93,7 +93,7 @@ func runAitableExportData(cmd *cobra.Command, runner executor.Runner) error { return apperrors.NewValidation("--scope is required") } if strings.TrimSpace(format) == "" { - return apperrors.NewValidation("--format is required") + return apperrors.NewValidation("--export-format is required") } params["scope"] = scope params["format"] = format From 8b771fd5ce9d14641823e15760785639f9cc7ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:21:57 +0800 Subject: [PATCH 3/3] docs(aitable): align export skill docs with the --export-format rename The export flag was renamed from --format to --export-format in aitable_export_import.go (to unshadow the global -f/--format output flag), but the skill reference docs still taught the old --format excel form, which now collides with the global output-format flag. Update the four mono/multi aitable references to match the code. --- .../references/products/aitable/aitable-best-practices.md | 6 +++--- .../references/products/aitable/aitable-export-import.md | 4 ++-- .../references/aitable/aitable-best-practices.md | 6 +++--- .../references/aitable/aitable-export-import.md | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/skills/mono/references/products/aitable/aitable-best-practices.md b/skills/mono/references/products/aitable/aitable-best-practices.md index 2c9b2200..cd9c8ff4 100644 --- a/skills/mono/references/products/aitable/aitable-best-practices.md +++ b/skills/mono/references/products/aitable/aitable-best-practices.md @@ -24,7 +24,7 @@ |---------|----------|----------| | 查看几条数据 | `dws aitable record query --base-id --table-id ` | 不要默认 `--all` | | 全量拉取/统计 | `dws aitable record query --base-id --table-id --all` | 不要手动循环 cursor | -| 全量导出 | `dws aitable export data --base-id --scope all --format excel` | 不要 `--all` 拉全量再写文件 | +| 全量导出 | `dws aitable export data --base-id --scope all --export-format excel` | 不要 `--all` 拉全量再写文件 | | 文件级导入 | `dws aitable import upload --base-id --file-name data.xlsx --file-size <字节数>` + `dws aitable import data --import-id ` | 不要手动解析 xlsx 再逐条写入 | | 批量写入多条不同数据 | `dws aitable record create --base-id --table-id --records '[{"cells":{"":"值"}}]'` | 不要一次超过 100 条 | | 批量给多条记录写同一组值 | `dws aitable record update --base-id --table-id --records '[{"recordId":"rec1","cells":{"":"值"}},{"recordId":"rec2","cells":{"":"值"}}]'` | 不要使用隐藏兼容命令 | @@ -49,11 +49,11 @@ ## 5. 导入导出与异步任务 -- `export data` 的 `--format` 是导出格式,不要在此命令上追加全局 `--format json`。 +- `export data` 的导出格式用 `--export-format`(如 `--export-format excel`);`--format` 在这里是全局输出格式,两者不要混用。 - 创建导出任务: ```bash dws aitable export data --base-id --scope table --table-id \ - --format excel --timeout-ms 1000 + --export-format excel --timeout-ms 1000 ``` - 续等已有导出任务: ```bash diff --git a/skills/mono/references/products/aitable/aitable-export-import.md b/skills/mono/references/products/aitable/aitable-export-import.md index 2fc9b469..0afbf9f1 100644 --- a/skills/mono/references/products/aitable/aitable-export-import.md +++ b/skills/mono/references/products/aitable/aitable-export-import.md @@ -4,11 +4,11 @@ `export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。 -> ⚠️ **`export data` 的 `--format` 是导出格式**:需要导出 xlsx/附件时写 `--format excel` / `excel_and_attachment`。不要在这个命令上追加全局 `--format json`。 +> ⚠️ **`export data` 的导出格式用 `--export-format`**:需要导出 xlsx/附件时写 `--export-format excel` / `excel_and_attachment`。这里的 `--format` 是全局输出格式(json/table…),两者不要混用。 ```bash # 第一步:创建任务(按 scope 传必要参数) -dws aitable export data --base-id --scope table --table-id --format excel --timeout-ms 1000 +dws aitable export data --base-id --scope table --table-id --export-format excel --timeout-ms 1000 # 第二步:拿 taskId 继续轮询,直到返回 downloadUrl dws aitable export data --base-id --task-id --timeout-ms 3000 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-best-practices.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-best-practices.md index 2c9b2200..cd9c8ff4 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-best-practices.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-best-practices.md @@ -24,7 +24,7 @@ |---------|----------|----------| | 查看几条数据 | `dws aitable record query --base-id --table-id ` | 不要默认 `--all` | | 全量拉取/统计 | `dws aitable record query --base-id --table-id --all` | 不要手动循环 cursor | -| 全量导出 | `dws aitable export data --base-id --scope all --format excel` | 不要 `--all` 拉全量再写文件 | +| 全量导出 | `dws aitable export data --base-id --scope all --export-format excel` | 不要 `--all` 拉全量再写文件 | | 文件级导入 | `dws aitable import upload --base-id --file-name data.xlsx --file-size <字节数>` + `dws aitable import data --import-id ` | 不要手动解析 xlsx 再逐条写入 | | 批量写入多条不同数据 | `dws aitable record create --base-id --table-id --records '[{"cells":{"":"值"}}]'` | 不要一次超过 100 条 | | 批量给多条记录写同一组值 | `dws aitable record update --base-id --table-id --records '[{"recordId":"rec1","cells":{"":"值"}},{"recordId":"rec2","cells":{"":"值"}}]'` | 不要使用隐藏兼容命令 | @@ -49,11 +49,11 @@ ## 5. 导入导出与异步任务 -- `export data` 的 `--format` 是导出格式,不要在此命令上追加全局 `--format json`。 +- `export data` 的导出格式用 `--export-format`(如 `--export-format excel`);`--format` 在这里是全局输出格式,两者不要混用。 - 创建导出任务: ```bash dws aitable export data --base-id --scope table --table-id \ - --format excel --timeout-ms 1000 + --export-format excel --timeout-ms 1000 ``` - 续等已有导出任务: ```bash diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md index 2fc9b469..0afbf9f1 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md @@ -4,11 +4,11 @@ `export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。 -> ⚠️ **`export data` 的 `--format` 是导出格式**:需要导出 xlsx/附件时写 `--format excel` / `excel_and_attachment`。不要在这个命令上追加全局 `--format json`。 +> ⚠️ **`export data` 的导出格式用 `--export-format`**:需要导出 xlsx/附件时写 `--export-format excel` / `excel_and_attachment`。这里的 `--format` 是全局输出格式(json/table…),两者不要混用。 ```bash # 第一步:创建任务(按 scope 传必要参数) -dws aitable export data --base-id --scope table --table-id --format excel --timeout-ms 1000 +dws aitable export data --base-id --scope table --table-id --export-format excel --timeout-ms 1000 # 第二步:拿 taskId 继续轮询,直到返回 downloadUrl dws aitable export data --base-id --task-id --timeout-ms 3000