add command

This commit is contained in:
玉澜 committed 2026-04-03 20:01:05 +08:00
1 parent afdd47b5a5
commit 1727744691
7 files changed
+957 -15

No files matched your search

+43 -6
View File
@@ -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
}
+727
View File
@@ -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
}
+34 -9
View File
@@ -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
}
}
}
}
+38
View File
@@ -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)
}
}
+40
View File
@@ -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())
}
+41
View File
@@ -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)
}
}
}
}
+34
View File
@@ -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))
}