diff --git a/internal/helpers/aitable.go b/internal/helpers/aitable.go index c315037d..bf244dca 100644 --- a/internal/helpers/aitable.go +++ b/internal/helpers/aitable.go @@ -66,7 +66,14 @@ func (aitableHandler) Command(runner executor.Runner) *cobra.Command { return cmd.Help() }, } - base.AddCommand(newAitableBaseDeleteCommand(runner)) + base.AddCommand( + newAitableBaseListCommand(runner), + newAitableBaseSearchCommand(runner), + newAitableBaseGetCommand(runner), + newAitableBaseCreateCommand(runner), + newAitableBaseUpdateCommand(runner), + newAitableBaseDeleteCommand(runner), + ) table := &cobra.Command{ Use: "table", @@ -78,7 +85,12 @@ func (aitableHandler) Command(runner executor.Runner) *cobra.Command { return cmd.Help() }, } - table.AddCommand(newAitableTableDeleteCommand(runner)) + table.AddCommand( + newAitableTableGetCommand(runner), + newAitableTableCreateCommand(runner), + newAitableTableUpdateCommand(runner), + newAitableTableDeleteCommand(runner), + ) field := &cobra.Command{ Use: "field", @@ -90,7 +102,12 @@ func (aitableHandler) Command(runner executor.Runner) *cobra.Command { return cmd.Help() }, } - field.AddCommand(newAitableFieldDeleteCommand(runner)) + field.AddCommand( + newAitableFieldGetCommand(runner), + newAitableFieldCreateCommand(runner), + newAitableFieldUpdateCommand(runner), + newAitableFieldDeleteCommand(runner), + ) record := &cobra.Command{ Use: "record", @@ -102,7 +119,24 @@ func (aitableHandler) Command(runner executor.Runner) *cobra.Command { return cmd.Help() }, } - record.AddCommand(newAitableRecordDeleteCommand(runner)) + record.AddCommand( + newAitableRecordQueryCommand(runner), + newAitableRecordCreateCommand(runner), + newAitableRecordUpdateCommand(runner), + newAitableRecordDeleteCommand(runner), + ) + + template := &cobra.Command{ + Use: "template", + Short: i18n.T("模板搜索"), + Args: cobra.NoArgs, + TraverseChildren: true, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + template.AddCommand(newAitableTemplateSearchCommand(runner)) attachment := &cobra.Command{ Use: "attachment", @@ -114,9 +148,12 @@ func (aitableHandler) Command(runner executor.Runner) *cobra.Command { return cmd.Help() }, } - attachment.AddCommand(newAITableUploadFileCommand(runner)) + attachment.AddCommand( + newAITableAttachmentUploadCommand(runner), + newAITableUploadFileCommand(runner), + ) - root.AddCommand(base, table, field, record, attachment) + root.AddCommand(base, table, field, record, template, attachment) return root } diff --git a/internal/helpers/aitable_commands.go b/internal/helpers/aitable_commands.go new file mode 100644 index 00000000..237c40ee --- /dev/null +++ b/internal/helpers/aitable_commands.go @@ -0,0 +1,727 @@ +// 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 helpers + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd" + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/i18n" + "github.com/spf13/cobra" +) + +// ── base ──────────────────────────────────────────────────── + +func newAitableBaseListCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: i18n.T("获取 AI 表格列表"), + Example: " dws aitable base list\n dws aitable base list --limit 5 --cursor NEXT_CURSOR", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + params := map[string]any{} + if limit, _ := cmd.Flags().GetInt("limit"); limit > 0 { + params["limit"] = limit + } + if cursor := aitableStringFlag(cmd, "cursor"); cursor != "" { + params["cursor"] = cursor + } + return runAitableTool(cmd, runner, "list_bases", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().Int("limit", 0, i18n.T("每页数量")) + cmd.Flags().String("cursor", "", i18n.T("分页游标")) + return cmd +} + +func newAitableBaseSearchCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "search", + Short: i18n.T("搜索 AI 表格"), + Example: " dws aitable base search --query 项目管理", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + query := aitableFlagOrFallback(cmd, "query", "keyword") + if query == "" { + return apperrors.NewValidation("--query is required") + } + params := map[string]any{"query": query} + if cursor := aitableStringFlag(cmd, "cursor"); cursor != "" { + params["cursor"] = cursor + } + return runAitableTool(cmd, runner, "search_bases", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("query", "", i18n.T("Base 名称关键词 (必填)")) + cmd.Flags().String("keyword", "", i18n.T("--query 的别名")) + _ = cmd.Flags().MarkHidden("keyword") + cmd.Flags().String("cursor", "", i18n.T("分页游标")) + return cmd +} + +func newAitableBaseGetCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: i18n.T("获取 AI 表格信息"), + Example: " dws aitable base get --base-id BASE_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + return runAitableTool(cmd, runner, "get_base", map[string]any{ + "baseId": baseID, + }) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + return cmd +} + +func newAitableBaseCreateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: i18n.T("创建 AI 表格"), + Example: " dws aitable base create --name 项目跟踪", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + name, err := aitableRequiredFlag(cmd, "name") + if err != nil { + return err + } + params := map[string]any{"baseName": name} + if templateID := aitableStringFlag(cmd, "template-id"); templateID != "" { + params["templateId"] = templateID + } + return runAitableTool(cmd, runner, "create_base", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("name", "", i18n.T("Base 名称 (必填)")) + cmd.Flags().String("template-id", "", i18n.T("模板 ID")) + return cmd +} + +func newAitableBaseUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: i18n.T("更新 AI 表格"), + Example: " dws aitable base update --base-id BASE_ID --name 新名称", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + name, err := aitableRequiredFlag(cmd, "name") + if err != nil { + return err + } + params := map[string]any{ + "baseId": baseID, + "newBaseName": name, + } + if desc := aitableStringFlag(cmd, "desc"); desc != "" { + params["description"] = desc + } + return runAitableTool(cmd, runner, "update_base", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("name", "", i18n.T("新名称 (必填)")) + cmd.Flags().String("desc", "", i18n.T("备注文本")) + return cmd +} + +// ── table ─────────────────────────────────────────────────── + +func newAitableTableGetCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: i18n.T("获取数据表"), + Example: " dws aitable table get --base-id BASE_ID\n dws aitable table get --base-id BASE_ID --table-ids tbl1,tbl2", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + params := map[string]any{"baseId": baseID} + if tableIDs := aitableStringFlag(cmd, "table-ids"); tableIDs != "" { + params["tableIds"] = parseAitableCSVValues(tableIDs) + } + return runAitableTool(cmd, runner, "get_tables", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("table-ids", "", i18n.T("Table ID 列表,逗号分隔")) + return cmd +} + +func newAitableTableCreateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: i18n.T("创建数据表"), + Example: " dws aitable table create --base-id BASE_ID --name 任务表 --fields '[{\"fieldName\":\"名称\",\"type\":\"text\"}]'", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + tableName := aitableFlagOrFallback(cmd, "name", "table-name") + if tableName == "" { + return apperrors.NewValidation("--name is required") + } + fieldsRaw, err := aitableRequiredFlag(cmd, "fields") + if err != nil { + return err + } + fields, err := parseAitableFieldsJSON(fieldsRaw) + if err != nil { + return err + } + return runAitableTool(cmd, runner, "create_table", map[string]any{ + "baseId": baseID, + "tableName": tableName, + "fields": fields, + }) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("name", "", i18n.T("表格名称 (必填)")) + cmd.Flags().String("table-name", "", i18n.T("--name 的别名")) + _ = cmd.Flags().MarkHidden("table-name") + cmd.Flags().String("fields", "", i18n.T("字段 JSON 数组 (必填)")) + return cmd +} + +func newAitableTableUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: i18n.T("更新数据表"), + Example: " dws aitable table update --base-id BASE_ID --table-id TABLE_ID --name 新表名", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + tableID, err := aitableRequiredFlag(cmd, "table-id") + if err != nil { + return err + } + name, err := aitableRequiredFlag(cmd, "name") + if err != nil { + return err + } + return runAitableTool(cmd, runner, "update_table", map[string]any{ + "baseId": baseID, + "tableId": tableID, + "newTableName": name, + }) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)")) + cmd.Flags().String("name", "", i18n.T("新表名 (必填)")) + return cmd +} + +// ── field ─────────────────────────────────────────────────── + +func newAitableFieldGetCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: i18n.T("获取字段详情"), + Example: " dws aitable field get --base-id BASE_ID --table-id TABLE_ID\n dws aitable field get --base-id BASE_ID --table-id TABLE_ID --field-ids fld1,fld2", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + tableID, err := aitableRequiredFlag(cmd, "table-id") + if err != nil { + return err + } + params := map[string]any{ + "baseId": baseID, + "tableId": tableID, + } + if fieldIDs := aitableStringFlag(cmd, "field-ids"); fieldIDs != "" { + params["fieldIds"] = parseAitableCSVValues(fieldIDs) + } + return runAitableTool(cmd, runner, "get_fields", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)")) + cmd.Flags().String("field-ids", "", i18n.T("Field ID 列表,逗号分隔")) + return cmd +} + +func newAitableFieldCreateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: i18n.T("创建字段"), + Example: " dws aitable field create --base-id BASE_ID --table-id TABLE_ID --fields '[{\"fieldName\":\"状态\",\"type\":\"singleSelect\"}]'", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + tableID, err := aitableRequiredFlag(cmd, "table-id") + if err != nil { + return err + } + + var fields []any + fieldsRaw := aitableStringFlag(cmd, "fields") + if fieldsRaw != "" { + fields, err = parseAitableFieldsJSON(fieldsRaw) + if err != nil { + return err + } + } else { + name, nameErr := aitableRequiredFlag(cmd, "name") + if nameErr != nil { + return apperrors.NewValidation("must specify either --fields or both --name and --type") + } + fieldType, typeErr := aitableRequiredFlag(cmd, "type") + if typeErr != nil { + return apperrors.NewValidation("must specify either --fields or both --name and --type") + } + field := map[string]any{ + "fieldName": name, + "type": fieldType, + } + if configRaw := aitableStringFlag(cmd, "config"); configRaw != "" { + configValue, err := parseAitableJSONObject(configRaw, "config") + if err != nil { + return err + } + field["config"] = configValue + } + fields = []any{field} + } + + return runAitableTool(cmd, runner, "create_fields", map[string]any{ + "baseId": baseID, + "tableId": tableID, + "fields": fields, + }) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)")) + cmd.Flags().String("fields", "", i18n.T("字段 JSON 数组")) + cmd.Flags().String("name", "", i18n.T("单字段名称")) + cmd.Flags().String("type", "", i18n.T("单字段类型")) + cmd.Flags().String("config", "", i18n.T("字段配置 JSON")) + return cmd +} + +func newAitableFieldUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: i18n.T("更新字段"), + Example: " dws aitable field update --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --name 新字段名", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + tableID, err := aitableRequiredFlag(cmd, "table-id") + if err != nil { + return err + } + fieldID, err := aitableRequiredFlag(cmd, "field-id") + if err != nil { + return err + } + name := aitableStringFlag(cmd, "name") + configRaw := aitableStringFlag(cmd, "config") + if name == "" && configRaw == "" { + return apperrors.NewValidation("at least one of --name or --config is required") + } + + params := map[string]any{ + "baseId": baseID, + "tableId": tableID, + "fieldId": fieldID, + } + if name != "" { + params["newFieldName"] = name + } + if configRaw != "" { + configValue, err := parseAitableJSONObject(configRaw, "config") + if err != nil { + return err + } + params["config"] = configValue + } + return runAitableTool(cmd, runner, "update_field", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)")) + cmd.Flags().String("field-id", "", i18n.T("Field ID (必填)")) + cmd.Flags().String("name", "", i18n.T("新字段名")) + cmd.Flags().String("config", "", i18n.T("字段配置 JSON")) + return cmd +} + +// ── record ────────────────────────────────────────────────── + +func newAitableRecordQueryCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "query", + Short: i18n.T("查询记录"), + Example: " dws aitable record query --base-id BASE_ID --table-id TABLE_ID --keyword 关键词 --limit 50", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + tableID, err := aitableRequiredFlag(cmd, "table-id") + if err != nil { + return err + } + + params := map[string]any{ + "baseId": baseID, + "tableId": tableID, + } + if recordIDs := aitableStringFlag(cmd, "record-ids"); recordIDs != "" { + params["recordIds"] = parseAitableCSVValues(recordIDs) + } + if fieldIDs := aitableStringFlag(cmd, "field-ids"); fieldIDs != "" { + params["fieldIds"] = parseAitableCSVValues(fieldIDs) + } + if filtersRaw := aitableStringFlag(cmd, "filters"); filtersRaw != "" { + filters, err := parseAitableJSONObject(filtersRaw, "filters") + if err != nil { + return err + } + params["filters"] = filters + } + if sortRaw := aitableStringFlag(cmd, "sort"); sortRaw != "" { + sortValue, err := parseAitableJSONArray(sortRaw, "sort") + if err != nil { + return err + } + params["sort"] = sortValue + } + if keyword := aitableFlagOrFallback(cmd, "query", "keyword"); keyword != "" { + params["keyword"] = keyword + } + if limit, _ := cmd.Flags().GetInt("limit"); limit > 0 { + params["limit"] = limit + } + if cursor := aitableStringFlag(cmd, "cursor"); cursor != "" { + params["cursor"] = cursor + } + return runAitableTool(cmd, runner, "query_records", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)")) + cmd.Flags().String("record-ids", "", i18n.T("Record ID 列表,逗号分隔")) + cmd.Flags().String("field-ids", "", i18n.T("Field ID 列表,逗号分隔")) + cmd.Flags().String("filters", "", i18n.T("过滤条件 JSON")) + cmd.Flags().String("sort", "", i18n.T("排序 JSON 数组")) + cmd.Flags().String("query", "", i18n.T("全文关键词")) + cmd.Flags().String("keyword", "", i18n.T("--query 的别名")) + _ = cmd.Flags().MarkHidden("keyword") + cmd.Flags().Int("limit", 0, i18n.T("单次最大记录数")) + cmd.Flags().String("cursor", "", i18n.T("分页游标")) + return cmd +} + +func newAitableRecordCreateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: i18n.T("新增记录"), + Example: " dws aitable record create --base-id BASE_ID --table-id TABLE_ID --records '[{\"cells\":{\"fld1\":\"hello\"}}]'", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + tableID, err := aitableRequiredFlag(cmd, "table-id") + if err != nil { + return err + } + recordsRaw, err := aitableRequiredFlag(cmd, "records") + if err != nil { + return err + } + records, err := parseAitableJSONArray(recordsRaw, "records") + if err != nil { + return err + } + return runAitableTool(cmd, runner, "create_records", map[string]any{ + "baseId": baseID, + "tableId": tableID, + "records": records, + }) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)")) + cmd.Flags().String("records", "", i18n.T("记录 JSON 数组 (必填)")) + return cmd +} + +func newAitableRecordUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: i18n.T("更新记录"), + Example: " dws aitable record update --base-id BASE_ID --table-id TABLE_ID --records '[{\"recordId\":\"rec1\",\"cells\":{\"fld1\":\"updated\"}}]'", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + tableID, err := aitableRequiredFlag(cmd, "table-id") + if err != nil { + return err + } + recordsRaw, err := aitableRequiredFlag(cmd, "records") + if err != nil { + return err + } + records, err := parseAitableJSONArray(recordsRaw, "records") + if err != nil { + return err + } + return runAitableTool(cmd, runner, "update_records", map[string]any{ + "baseId": baseID, + "tableId": tableID, + "records": records, + }) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)")) + cmd.Flags().String("records", "", i18n.T("记录 JSON 数组 (必填)")) + return cmd +} + +// ── template ──────────────────────────────────────────────── + +func newAitableTemplateSearchCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "search", + Short: i18n.T("搜索模板"), + Example: " dws aitable template search --query 项目管理", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + query := aitableFlagOrFallback(cmd, "query", "keyword") + if query == "" { + return apperrors.NewValidation("--query is required") + } + params := map[string]any{"query": query} + if limit, _ := cmd.Flags().GetInt("limit"); limit > 0 { + params["limit"] = limit + } + if cursor := aitableStringFlag(cmd, "cursor"); cursor != "" { + params["cursor"] = cursor + } + return runAitableTool(cmd, runner, "search_templates", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("query", "", i18n.T("模板关键词 (必填)")) + cmd.Flags().String("keyword", "", i18n.T("--query 的别名")) + _ = cmd.Flags().MarkHidden("keyword") + cmd.Flags().Int("limit", 0, i18n.T("每页数量")) + cmd.Flags().String("cursor", "", i18n.T("分页游标")) + return cmd +} + +// ── attachment ────────────────────────────────────────────── + +func newAITableAttachmentUploadCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "upload", + Short: i18n.T("准备附件上传"), + Example: " dws aitable attachment upload --base-id BASE_ID --file-name report.pdf --size 1024", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlag(cmd, "base-id") + if err != nil { + return err + } + fileName, err := aitableRequiredFlag(cmd, "file-name") + if err != nil { + return err + } + params := map[string]any{ + "baseId": baseID, + "fileName": fileName, + } + if size, _ := cmd.Flags().GetInt64("size"); size > 0 { + params["size"] = size + } + if mimeType := aitableStringFlag(cmd, "mime-type"); mimeType != "" { + params["mimeType"] = mimeType + } + return runAitableTool(cmd, runner, "prepare_attachment_upload", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + cmd.Flags().String("file-name", "", i18n.T("文件名 (必填)")) + cmd.Flags().Int64("size", 0, i18n.T("文件大小(字节)")) + cmd.Flags().String("mime-type", "", i18n.T("文件 MIME Type")) + return cmd +} + +// ── helpers ──────────────────────────────────────────────── + +func runAitableTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error { + invocation := executor.NewHelperInvocation( + cobracmd.LegacyCommandPath(cmd), + "aitable", + tool, + params, + ) + invocation.DryRun = commandDryRun(cmd) + result, err := runner.Run(cmd.Context(), invocation) + if err != nil { + return err + } + return writeCommandPayload(cmd, result) +} + +func aitableStringFlag(cmd *cobra.Command, name string) string { + if cmd == nil { + return "" + } + if value, err := cmd.Flags().GetString(name); err == nil && strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + if value, err := cmd.InheritedFlags().GetString(name); err == nil && strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + return "" +} + +func aitableFlagOrFallback(cmd *cobra.Command, primary string, aliases ...string) string { + if value := aitableStringFlag(cmd, primary); value != "" { + return value + } + for _, alias := range aliases { + if value := aitableStringFlag(cmd, alias); value != "" { + return value + } + } + return "" +} + +func aitableRequiredFlag(cmd *cobra.Command, name string) (string, error) { + if value := aitableStringFlag(cmd, name); value != "" { + return value, nil + } + return "", apperrors.NewValidation(fmt.Sprintf("--%s is required", name)) +} + +func aitableRequiredFlagOrFallback(cmd *cobra.Command, primary string, aliases ...string) (string, error) { + if value := aitableFlagOrFallback(cmd, primary, aliases...); value != "" { + return value, nil + } + return "", apperrors.NewValidation(fmt.Sprintf("--%s is required", primary)) +} + +func parseAitableCSVValues(raw string) []string { + parts := strings.Split(raw, ",") + values := make([]string, 0, len(parts)) + for _, part := range parts { + if trimmed := strings.TrimSpace(part); trimmed != "" { + values = append(values, trimmed) + } + } + return values +} + +func parseAitableFieldsJSON(raw string) ([]any, error) { + var fields []any + if err := json.Unmarshal([]byte(raw), &fields); err == nil { + return fields, nil + } + var wrapper map[string]any + if err := json.Unmarshal([]byte(raw), &wrapper); err == nil { + if wrappedFields, ok := wrapper["fields"].([]any); ok { + return wrappedFields, nil + } + } + return nil, apperrors.NewValidation("--fields JSON parse failed: expect a JSON array") +} + +func parseAitableJSONArray(raw, flagName string) ([]any, error) { + var value []any + if err := json.Unmarshal([]byte(raw), &value); err != nil { + return nil, apperrors.NewValidation(fmt.Sprintf("--%s JSON parse failed: %v", flagName, err)) + } + return value, nil +} + +func parseAitableJSONObject(raw, flagName string) (map[string]any, error) { + var value map[string]any + if err := json.Unmarshal([]byte(raw), &value); err != nil { + return nil, apperrors.NewValidation(fmt.Sprintf("--%s JSON parse failed: %v", flagName, err)) + } + return value, nil +} diff --git a/internal/output/formatter.go b/internal/output/formatter.go index 792b2145..dbed8105 100644 --- a/internal/output/formatter.go +++ b/internal/output/formatter.go @@ -148,45 +148,70 @@ func WriteFiltered(w io.Writer, format Format, payload any, fields, jq string) e } // ResolveFields extracts the --fields flag value from the command. +// It ensures that we do not mistakenly grab a business parameter also named "fields" +// by matching the flag's usage string against the global root definition. func ResolveFields(cmd *cobra.Command) string { if cmd == nil { return "" } + rootFlags := rootPersistentFlags(cmd) + if rootFlags == nil { + return "" + } + globalFlag := rootFlags.Lookup("fields") + if globalFlag == nil { + return "" + } + for _, flags := range []*pflag.FlagSet{ cmd.Flags(), cmd.InheritedFlags(), - rootPersistentFlags(cmd), + rootFlags, } { if flags == nil { continue } if f := flags.Lookup("fields"); f != nil && f.Changed { - if v, err := flags.GetString("fields"); err == nil { - return v + // To avoid collision with business flags (e.g. table create --fields), + // verify this flag shares the same usage string as the global one. + if f.Usage == globalFlag.Usage { + if v, err := flags.GetString("fields"); err == nil { + return v + } } } } return "" } -// ResolveJQ extracts the --jq flag value from the command. It checks -// local flags, inherited flags, and root persistent flags because -// --jq is registered as a root PersistentFlag. +// ResolveJQ extracts the --jq flag value from the command. It ensures +// that we only grab the global output filter, not a similarly named business parameter. func ResolveJQ(cmd *cobra.Command) string { if cmd == nil { return "" } + rootFlags := rootPersistentFlags(cmd) + if rootFlags == nil { + return "" + } + globalFlag := rootFlags.Lookup("jq") + if globalFlag == nil { + return "" + } + for _, flags := range []*pflag.FlagSet{ cmd.Flags(), cmd.InheritedFlags(), - rootPersistentFlags(cmd), + rootFlags, } { if flags == nil { continue } if f := flags.Lookup("jq"); f != nil && f.Changed { - if v, err := flags.GetString("jq"); err == nil { - return v + if f.Usage == globalFlag.Usage { + if v, err := flags.GetString("jq"); err == nil { + return v + } } } } diff --git a/internal/output/formatter_custom_test.go b/internal/output/formatter_custom_test.go new file mode 100644 index 00000000..62427e43 --- /dev/null +++ b/internal/output/formatter_custom_test.go @@ -0,0 +1,38 @@ +package output + +import ( + "github.com/spf13/cobra" + "testing" +) + +func TestResolveFieldsShadowing(t *testing.T) { + rootCmd := &cobra.Command{Use: "dws"} + var globalFields string + // Register the global persistent flag. + rootCmd.PersistentFlags().StringVar(&globalFields, "fields", "", "筛选输出字段 (逗号分隔, 如: name,id,status)") + + // 1. Normal command that relies on the global output filter + normalCmd := &cobra.Command{Use: "normal"} + rootCmd.AddCommand(normalCmd) + rootCmd.SetArgs([]string{"normal", "--fields", "data,status"}) + rootCmd.Execute() + + if fields := ResolveFields(normalCmd); fields != "data,status" { + t.Errorf("expected 'data,status' for normal cmd, got %q", fields) + } + + // 2. Command that shadows the global format flag with its own local business logic + bizCmd := &cobra.Command{Use: "biz"} + var localFields string + bizCmd.Flags().StringVar(&localFields, "fields", "", "JSON string array of objects") + rootCmd.AddCommand(bizCmd) + + // Reset + rootCmd.SetArgs([]string{"biz", "--fields", "[\"fake\"]"}) + rootCmd.Execute() + + // It should now correctly ignore the localized fields parameter! + if fields := ResolveFields(bizCmd); fields != "" { + t.Errorf("expected empty fields for shadowed cmd since it's a business param, got %q", fields) + } +} diff --git a/internal/output/unwrap_test.go b/internal/output/unwrap_test.go new file mode 100644 index 00000000..7b1b9747 --- /dev/null +++ b/internal/output/unwrap_test.go @@ -0,0 +1,40 @@ +package output + +import ( + "bytes" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" + "testing" +) + +func TestUnwrapAndWrite(t *testing.T) { + // Simulate the Result + result := executor.Result{ + Invocation: executor.Invocation{ + Implemented: true, + Kind: "compat_invocation", + }, + Response: map[string]any{ + "endpoint": "https://mcp-gw", + "content": map[string]any{}, + }, + } + + var buf bytes.Buffer + Write(&buf, FormatJSON, result) + + t.Logf("Output: %s", buf.String()) + + resultNil := executor.Result{ + Invocation: executor.Invocation{ + Implemented: true, + Kind: "compat_invocation", + }, + Response: map[string]any{ + "endpoint": "https://mcp-gw", + "content": nil, + }, + } + buf.Reset() + Write(&buf, FormatJSON, resultNil) + t.Logf("Output nil: %s", buf.String()) +} diff --git a/test/cli_compat/debug_test.go b/test/cli_compat/debug_test.go new file mode 100644 index 00000000..42863da1 --- /dev/null +++ b/test/cli_compat/debug_test.go @@ -0,0 +1,41 @@ +package cli_compat_test + +import ( + "bytes" + "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" +) + +func TestDebugAitableTableCreate(t *testing.T) { + _ = setupTestDeps(t, "aitable") + root := app.NewRootCommand() + + cliArgs := []string{"-f", "json", "aitable", "table", "create", + "--base-id", "B1", "--name", "任务表", + "--fields", `[{"fieldName":"名称","type":"text"}]`, + } + + var out bytes.Buffer + var errOut bytes.Buffer + root.SetOut(&out) + root.SetErr(&errOut) + root.SetArgs(cliArgs) + + err := root.Execute() + t.Logf("Execute error: %v", err) + t.Logf("Stdout: [%s]", out.String()) + t.Logf("Stderr: [%s]", errOut.String()) + + // Check all aitable subcommands + aitableCmd, _, _ := root.Find([]string{"aitable"}) + if aitableCmd != nil { + t.Logf("aitable subcommands:") + for _, grp := range aitableCmd.Commands() { + t.Logf(" %s:", grp.Use) + for _, sub := range grp.Commands() { + t.Logf(" %s (hidden=%v)", sub.Use, sub.Hidden) + } + } + } +} diff --git a/test_parse.go b/test_parse.go new file mode 100644 index 00000000..d06abc87 --- /dev/null +++ b/test_parse.go @@ -0,0 +1,34 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +type ContentBlock struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` +} + +func main() { + data := []byte(`{"content":[{"type":"text","text":"{\"summary\":\"...\",\"data\":{\"tableId\":\"abc\"},\"status\":\"success\"}"}],"structuredContent":{"summary":"...","data":{"tableId":"abc"},"status":"success"},"isError":false}`) + + type rawResult struct { + Content json.RawMessage `json:"content"` + StructuredContent map[string]any `json:"structuredContent"` + IsError bool `json:"isError,omitempty"` + } + + var raw rawResult + _ = json.Unmarshal(data, &raw) + + fmt.Printf("raw.Content string: %s\n", string(raw.Content)) + + var object map[string]any + errMap := json.Unmarshal(raw.Content, &object) + fmt.Printf("errMap: %v\n", errMap) + + var blocks []ContentBlock + errBlocks := json.Unmarshal(raw.Content, &blocks) + fmt.Printf("errBlocks: %v, len(blocks): %d\n", errBlocks, len(blocks)) +}