Switches the discovery version code bamboo -> cedar and aligns the open edition CLI with dws-wukong across communication (calendar book/acl/attendee, minutes tag, mail folder/template/contact, chat file upload, todo add-attachment, attendance transforms) and structured-office (aitable advperm/view/section/workflow/record, sheet/drive/wiki/doc) domains. Includes output-envelope parity, parse_bool/attendance_class_check_time transforms, --calendar-id support, CHANGELOG 1.0.43 and README command-index refresh. cedar config validated on pre and prod endpoints.
2176 lines
74 KiB
Go
2176 lines
74 KiB
Go
// 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 (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"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/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/paging"
|
||
"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 == "" {
|
||
params := map[string]any{}
|
||
if cursor := aitableStringFlag(cmd, "cursor"); cursor != "" {
|
||
params["cursor"] = cursor
|
||
}
|
||
return runAitableTool(cmd, runner, "list_bases", params)
|
||
}
|
||
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", "id")
|
||
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 (必填)"))
|
||
addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名")
|
||
addAitableHiddenStringFlag(cmd, "id", "--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
|
||
}
|
||
if folderID := aitableStringFlag(cmd, "folder-id"); folderID != "" {
|
||
params["folderId"] = folderID
|
||
}
|
||
// dry-run 或带模板:保持单步 create_base 语义。
|
||
if commandDryRun(cmd) {
|
||
return runAitableTool(cmd, runner, "create_base", params)
|
||
}
|
||
result, err := runAitableProductToolResult(cmd, runner, "aitable", "create_base", params)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// 默认表由服务端按 scenario 决定:wukong 场景建、openClaw 不建。
|
||
// 为与 wukong 行为对齐,无模板时在 CLI 侧兜底补建一张默认表,
|
||
// 使新 base 立即可用(含 tableId)。输出仍保持原 create_base 结果,
|
||
// 不把内部补建步骤暴露给调用方。
|
||
if _, hasTemplate := params["templateId"]; !hasTemplate {
|
||
if baseID := findStringDeep(result.Response, "baseId", "baseID"); baseID != "" {
|
||
if _, terr := runAitableProductToolResult(cmd, runner, "aitable", "create_table", map[string]any{
|
||
"baseId": baseID,
|
||
"tableName": "表格1",
|
||
"fields": []any{map[string]any{
|
||
"fieldName": "标题",
|
||
"type": "text",
|
||
}},
|
||
}); terr != nil {
|
||
fmt.Fprintf(cmd.ErrOrStderr(), "warning: created base %s but default table creation failed: %v\n", baseID, terr)
|
||
}
|
||
}
|
||
}
|
||
return writeCommandPayload(cmd, result)
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
cmd.Flags().String("name", "", i18n.T("Base 名称 (必填)"))
|
||
cmd.Flags().String("template-id", "", i18n.T("模板 ID"))
|
||
cmd.Flags().String("folder-id", "", i18n.T("父文件夹 ID"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableBaseCopyCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "copy",
|
||
Short: i18n.T("复制 AI 表格"),
|
||
Long: i18n.T(`复制 AI 表格到指定目录。支持完整复制或仅复制结构(不含数据)。
|
||
复制操作会创建一个新的 Base,包含原 Base 的表、字段、视图等配置。
|
||
如果选择仅复制结构(--only-struct),则不会复制实际的记录数据。
|
||
|
||
权限要求:需要对源 Base 有"阅读"权限,且对目标文件夹有"编辑"权限。
|
||
|
||
注意:--target-folder-id 参数如果传入的是文档/文件夹 URL(如 https://alidocs.dingtalk.com/i/nodes/xxx),
|
||
需要先调用文档的 dws 命令(如 dws doc info --node URL)获取 dentryUuid,再将 dentryUuid 传入本命令。
|
||
MCP 层不会会自动解析 URL,必须直接传入 dentryUuid 以避免报错。`),
|
||
Example: ` dws aitable base copy --base-id BASE_ID --target-folder-id FOLDER_ID
|
||
dws aitable base copy --base-id BASE_ID --target-folder-id FOLDER_ID --only-struct
|
||
# 查询 baseId: dws aitable base list
|
||
# 查询 folderId: dws doc list --folder PARENT_FOLDER_ID`,
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, err := aitableRequiredFlag(cmd, "base-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
targetFolderID, err := aitableRequiredFlag(cmd, "target-folder-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
onlyCopyMeta, err := cmd.Flags().GetBool("only-struct")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runAitableTool(cmd, runner, "copy_base", map[string]any{
|
||
"baseId": baseID,
|
||
"targetFolderId": targetFolderID,
|
||
"onlyCopyMeta": onlyCopyMeta,
|
||
})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
cmd.Flags().String("base-id", "", i18n.T("源 Base ID (必填)"))
|
||
cmd.Flags().String("target-folder-id", "", i18n.T("目标文件夹 ID (必填)"))
|
||
cmd.Flags().Bool("only-struct", false, i18n.T("是否仅复制结构(不含数据),默认 false 表示完整复制"))
|
||
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",
|
||
Aliases: []string{"list"},
|
||
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 := aitableStringFlag(cmd, "fields")
|
||
if strings.TrimSpace(fieldsRaw) == "" {
|
||
fieldsRaw = "[]"
|
||
}
|
||
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",
|
||
Aliases: []string{"list"},
|
||
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 := aitableFlagOrFallback(cmd, "name", "field-name")
|
||
fieldType := aitableFlagOrFallback(cmd, "type", "field-type")
|
||
if name == "" || fieldType == "" {
|
||
return apperrors.NewValidation("must specify either --fields or both --name and --type")
|
||
}
|
||
field := map[string]any{
|
||
"fieldName": name,
|
||
"type": normalizeAitableFieldType(fieldType),
|
||
}
|
||
if configRaw := aitableStringFlag(cmd, "config"); configRaw != "" {
|
||
configValue, err := parseAitableJSONObject(configRaw, "config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
field["config"] = configValue
|
||
}
|
||
if aiConfigRaw := aitableStringFlag(cmd, "ai-config"); aiConfigRaw != "" {
|
||
aiConfigValue, err := parseAitableJSONObject(aiConfigRaw, "ai-config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
field["aiConfig"] = aiConfigValue
|
||
}
|
||
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 (必填)"))
|
||
addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名")
|
||
cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)"))
|
||
cmd.Flags().String("fields", "", i18n.T("字段 JSON 数组"))
|
||
cmd.Flags().String("name", "", i18n.T("单字段名称"))
|
||
addAitableHiddenStringFlag(cmd, "field-name", "--name 的兼容别名")
|
||
cmd.Flags().String("type", "", i18n.T("单字段类型"))
|
||
addAitableHiddenStringFlag(cmd, "field-type", "--type 的兼容别名")
|
||
cmd.Flags().String("config", "", i18n.T("字段配置 JSON"))
|
||
cmd.Flags().String("ai-config", "", i18n.T("AI 字段配置 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")
|
||
aiConfigRaw := aitableStringFlag(cmd, "ai-config")
|
||
if name == "" && configRaw == "" && aiConfigRaw == "" {
|
||
return apperrors.NewValidation("at least one of --name, --config or --ai-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
|
||
}
|
||
if aiConfigRaw != "" {
|
||
aiConfigValue, err := parseAitableJSONObject(aiConfigRaw, "ai-config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["aiConfig"] = aiConfigValue
|
||
}
|
||
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"))
|
||
cmd.Flags().String("ai-config", "", i18n.T("AI 字段配置 JSON"))
|
||
return cmd
|
||
}
|
||
|
||
// ── record ──────────────────────────────────────────────────
|
||
|
||
func newAitableRecordQueryCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "query",
|
||
Aliases: []string{"list"},
|
||
Short: i18n.T("查询记录"),
|
||
Long: i18n.T(`查询 AI 表格记录。
|
||
|
||
默认行为:返回单页数据(受 --limit 控制,未传时由服务端默认)。
|
||
传入 --all 时自动翻页累计全部数据,受 --page-limit 安全阀控制(默认 50 页)。
|
||
|
||
翻页输出契约(仅 --all 时):
|
||
- records - 累计所有页的记录
|
||
- hasMore - true 表示触发 page-limit 或中途出错,需用 --cursor <X> 续拉
|
||
- cursor - 续拉用的下一页 cursor(hasMore=true 时有效)
|
||
- partial - true 表示中途某页失败,records 是失败前已累计的数据
|
||
- pages - 实际翻了多少页`),
|
||
Example: ` dws aitable record query --base-id B --table-id T --limit 50
|
||
dws aitable record query --base-id B --table-id T --keyword 关键词
|
||
dws aitable record query --base-id B --table-id T --all
|
||
dws aitable record query --base-id B --table-id T --all --page-limit 0 # 不限制
|
||
dws aitable record query --base-id B --table-id T --all --cursor <SAVED> # 接续上次断点`,
|
||
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
|
||
}
|
||
|
||
// 构建单页查询参数
|
||
baseParams := map[string]any{
|
||
"baseId": baseID,
|
||
"tableId": tableID,
|
||
}
|
||
if recordIDs := aitableStringFlag(cmd, "record-ids"); recordIDs != "" {
|
||
baseParams["recordIds"] = parseAitableCSVValues(recordIDs)
|
||
}
|
||
if fieldIDs := aitableStringFlag(cmd, "field-ids"); fieldIDs != "" {
|
||
baseParams["fieldIds"] = parseAitableCSVValues(fieldIDs)
|
||
}
|
||
if filtersRaw := aitableStringFlag(cmd, "filters"); filtersRaw != "" {
|
||
filters, err := parseAitableJSONObject(filtersRaw, "filters")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := validateAitableFiltersStructure(filters); err != nil {
|
||
return err
|
||
}
|
||
baseParams["filters"] = normalizeAitableFilters(filters)
|
||
}
|
||
if sortRaw := aitableStringFlag(cmd, "sort"); sortRaw != "" {
|
||
sortValue, err := parseAitableJSONArray(sortRaw, "sort")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
baseParams["sort"] = normalizeAitableSort(sortValue)
|
||
}
|
||
if keyword := aitableFlagOrFallback(cmd, "query", "keyword"); keyword != "" {
|
||
baseParams["keyword"] = keyword
|
||
}
|
||
if limit, _ := cmd.Flags().GetInt("limit"); limit > 0 {
|
||
baseParams["limit"] = limit
|
||
} else if pageSize, _ := cmd.Flags().GetInt("page-size"); pageSize > 0 {
|
||
baseParams["limit"] = pageSize
|
||
}
|
||
|
||
fetchAll, _ := cmd.Flags().GetBool("all")
|
||
initialCursor := aitableFlagOrFallback(cmd, "cursor", "page-token")
|
||
|
||
// 非 --all 模式:保持原行为(单页查询)
|
||
if !fetchAll {
|
||
params := cloneMap(baseParams)
|
||
if initialCursor != "" {
|
||
params["cursor"] = initialCursor
|
||
}
|
||
return runAitableTool(cmd, runner, "query_records", params)
|
||
}
|
||
|
||
// --all 模式:用 pkg/paging 循环
|
||
return runRecordQueryAll(cmd, runner, baseParams, initialCursor)
|
||
},
|
||
}
|
||
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().Int("page-size", 0, i18n.T("--limit 的兼容别名"))
|
||
_ = cmd.Flags().MarkHidden("page-size")
|
||
cmd.Flags().String("cursor", "", i18n.T("分页游标"))
|
||
cmd.Flags().String("page-token", "", i18n.T("--cursor 的兼容别名"))
|
||
_ = cmd.Flags().MarkHidden("page-token")
|
||
cmd.Flags().Bool("all", false, i18n.T("自动翻页获取全部记录(受 --page-limit 上限)"))
|
||
cmd.Flags().Int("page-limit", 50, i18n.T("自动翻页最大页数(默认 50 ≈ 5000 条),0 表示无限制"))
|
||
return cmd
|
||
}
|
||
|
||
// cloneMap 浅拷贝 map[string]any,避免在循环中共享底层 map 导致 cursor 覆写问题。
|
||
func cloneMap(m map[string]any) map[string]any {
|
||
out := make(map[string]any, len(m))
|
||
for k, v := range m {
|
||
out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
func newAitableRecordGetCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "get",
|
||
Short: i18n.T("按 Record ID 获取记录"),
|
||
Example: " dws aitable record get --base-id BASE_ID --table-id TABLE_ID --record-ids rec1,rec2",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, err := aitableRequiredFlag(cmd, "base-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
tableID, err := aitableRequiredFlag(cmd, "table-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
recordIDsRaw, err := aitableRequiredFlag(cmd, "record-ids")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
recordIDs := parseAitableCSVValues(recordIDsRaw)
|
||
if len(recordIDs) == 0 {
|
||
return apperrors.NewValidation("--record-ids is required")
|
||
}
|
||
|
||
params := map[string]any{
|
||
"baseId": baseID,
|
||
"tableId": tableID,
|
||
"recordIds": recordIDs,
|
||
}
|
||
if fieldIDs := aitableStringFlag(cmd, "field-ids"); fieldIDs != "" {
|
||
params["fieldIds"] = parseAitableCSVValues(fieldIDs)
|
||
}
|
||
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 列表,逗号分隔"))
|
||
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
|
||
}
|
||
records, err := resolveAitableRecordCreateRecords(cmd)
|
||
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 数组 (必填)"))
|
||
cmd.Flags().String("records-file", "", i18n.T("从文件读取 records JSON"))
|
||
addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名")
|
||
addAitableHiddenStringFlag(cmd, "fields", "--records 的兼容别名")
|
||
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
|
||
}
|
||
records, err := resolveAitableRecordUpdateRecords(cmd)
|
||
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 数组 (必填)"))
|
||
cmd.Flags().String("records-file", "", i18n.T("从文件读取 records JSON"))
|
||
addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名")
|
||
addAitableHiddenStringFlag(cmd, "fields", "--records 的兼容别名")
|
||
addAitableHiddenStringFlag(cmd, "record-id", "--records 的兼容输入:单条记录 ID")
|
||
addAitableHiddenStringFlag(cmd, "cells", "--records 的兼容输入:单条记录 cells JSON 对象")
|
||
addAitableHiddenStringFlag(cmd, "data", "--records 的兼容输入:records 数组、单条 record 对象或 cells 对象")
|
||
return cmd
|
||
}
|
||
|
||
func newAitableRecordBatchUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "batch-update",
|
||
Hidden: true,
|
||
Short: i18n.T("批量更新记录(同一 cells 应用到多条 recordId)"),
|
||
Long: i18n.T(`将同一份 cells JSON 批量应用到多条记录上。
|
||
|
||
与 record update 的区别:
|
||
- record update 每条记录可有不同 cells,参数是 records JSON 数组
|
||
- record batch-update 所有记录共享同一份 cells,CLI 展开后调用 update_records
|
||
|
||
单次最多 100 条;超出请拆分多次调用。`),
|
||
Example: ` dws aitable record batch-update --base-id BASE_ID --table-id TABLE_ID --record-ids rec1,rec2 --cells '{"fldStatusId":"已完成"}'`,
|
||
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
|
||
}
|
||
recordIDsRaw, err := aitableRequiredFlag(cmd, "record-ids")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
recordIDs := parseAitableCSVValues(recordIDsRaw)
|
||
if len(recordIDs) == 0 {
|
||
return apperrors.NewValidation("--record-ids must not be empty")
|
||
}
|
||
if len(recordIDs) > 100 {
|
||
return apperrors.NewValidation(fmt.Sprintf("--record-ids exceeds limit: got %d, max 100", len(recordIDs)))
|
||
}
|
||
cellsRaw, err := aitableRequiredFlag(cmd, "cells")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cells, err := parseAitableJSONObject(cellsRaw, "cells")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(cells) == 0 {
|
||
return apperrors.NewValidation("--cells must not be empty")
|
||
}
|
||
records := make([]any, 0, len(recordIDs))
|
||
for _, recordID := range recordIDs {
|
||
records = append(records, map[string]any{
|
||
"recordId": recordID,
|
||
"cells": cells,
|
||
})
|
||
}
|
||
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 (必填)"))
|
||
addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名")
|
||
cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)"))
|
||
cmd.Flags().String("record-ids", "", i18n.T("Record ID 列表,逗号分隔 (必填)"))
|
||
cmd.Flags().String("cells", "", i18n.T("统一应用的 cells 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("准备附件上传 (仅返回 uploadUrl + fileToken, 不上传文件)"),
|
||
Long: i18n.T(`准备附件上传 — 3 步流程的第 1 步.
|
||
|
||
本命令只调用 prepare_attachment_upload, 返回 OSS 上传地址 uploadUrl 和 fileToken,
|
||
不实际上传文件二进制. 拿到响应后你还需要:
|
||
2. HTTP PUT 文件二进制 → uploadUrl
|
||
3. 把 fileToken 填到 record 的 attachment 字段, 格式 [{"fileToken":"ft_xxx"}]`),
|
||
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
|
||
}
|
||
if !cmd.Flags().Changed("size") {
|
||
return apperrors.NewValidation("--size is required")
|
||
}
|
||
size, _ := cmd.Flags().GetInt64("size")
|
||
if size <= 0 {
|
||
return apperrors.NewValidation("--size must be greater than 0")
|
||
}
|
||
params := map[string]any{
|
||
"baseId": baseID,
|
||
"fileName": fileName,
|
||
"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
|
||
}
|
||
|
||
// ── dashboard / chart ───────────────────────────────────────
|
||
|
||
func newAitableDashboardGetCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "get",
|
||
Short: i18n.T("获取仪表盘详情"),
|
||
Example: " dws aitable dashboard get --base-id BASE_ID --dashboard-id DASHBOARD_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
|
||
}
|
||
dashboardID, err := aitableRequiredFlag(cmd, "dashboard-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runAitableTool(cmd, runner, "get_dashboard", map[string]any{
|
||
"baseId": baseID,
|
||
"dashboardId": dashboardID,
|
||
})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableDashboardIDFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newAitableDashboardCreateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "create",
|
||
Short: i18n.T("创建仪表盘"),
|
||
Example: ` dws aitable dashboard create --base-id BASE_ID --config '{"dashboardName":"销售看板"}'
|
||
dws aitable dashboard create --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
|
||
}
|
||
config := map[string]any{}
|
||
if configRaw := aitableStringFlag(cmd, "config"); configRaw != "" {
|
||
parsed, err := parseAitableJSONObject(configRaw, "config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
config = parsed
|
||
}
|
||
if name := aitableStringFlag(cmd, "name"); name != "" {
|
||
config["dashboardName"] = name
|
||
}
|
||
if len(config) == 0 {
|
||
return apperrors.NewValidation("must specify either --config or --name")
|
||
}
|
||
return runAitableTool(cmd, runner, "create_dashboard", map[string]any{
|
||
"baseId": baseID,
|
||
"config": config,
|
||
})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)"))
|
||
addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名")
|
||
cmd.Flags().String("config", "", i18n.T("仪表盘配置 JSON,结构先看 dashboard config-example;可用 --name 简化创建"))
|
||
cmd.Flags().String("name", "", i18n.T("仪表盘名称(替代 --config 的简化创建)"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableDashboardUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "update",
|
||
Short: i18n.T("更新仪表盘"),
|
||
Example: ` dws aitable dashboard update --base-id BASE_ID --dashboard-id DASHBOARD_ID --config '{"dashboardName":"新名称"}'
|
||
dws aitable dashboard update --base-id BASE_ID --dashboard-id DASHBOARD_ID --name 新名称`,
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, dashboardID, err := requiredAitableDashboard(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
config := map[string]any{}
|
||
if configRaw := aitableStringFlag(cmd, "config"); configRaw != "" {
|
||
parsed, err := parseAitableJSONObject(configRaw, "config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
config = parsed
|
||
}
|
||
if name := aitableStringFlag(cmd, "name"); name != "" {
|
||
config["dashboardName"] = name
|
||
}
|
||
if len(config) == 0 {
|
||
return apperrors.NewValidation("must specify either --config or --name")
|
||
}
|
||
return runAitableTool(cmd, runner, "update_dashboard", map[string]any{
|
||
"baseId": baseID,
|
||
"dashboardId": dashboardID,
|
||
"config": config,
|
||
})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableDashboardIDFlags(cmd)
|
||
cmd.Flags().String("config", "", i18n.T("仪表盘配置 JSON,结构先看 dashboard config-example;可用 --name 简化改名"))
|
||
cmd.Flags().String("name", "", i18n.T("新仪表盘名称(替代 --config 的简化改名)"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableDashboardDeleteCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "delete",
|
||
Short: i18n.T("删除仪表盘"),
|
||
Long: i18n.T("删除指定仪表盘(高风险、不可逆)。使用 --yes 跳过确认。"),
|
||
Example: " dws aitable dashboard delete --base-id BASE_ID --dashboard-id DASHBOARD_ID --yes",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, dashboardID, err := requiredAitableDashboard(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !confirmDeletePrompt(cmd, i18n.T("仪表盘"), dashboardID) {
|
||
return nil
|
||
}
|
||
params := map[string]any{
|
||
"baseId": baseID,
|
||
"dashboardId": dashboardID,
|
||
}
|
||
if reason := aitableStringFlag(cmd, "reason"); reason != "" {
|
||
params["reason"] = reason
|
||
}
|
||
return runAitableTool(cmd, runner, "delete_dashboard", params)
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableDashboardIDFlags(cmd)
|
||
cmd.Flags().String("reason", "", i18n.T("删除原因(可选)"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableDashboardConfigExampleCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "config-example",
|
||
Short: i18n.T("获取仪表盘配置模板"),
|
||
Example: " dws aitable dashboard config-example",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
return runAitableTool(cmd, runner, "get_dashboard_config_example", map[string]any{})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newAitableDashboardShareGetCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "get",
|
||
Short: i18n.T("获取仪表盘分享配置"),
|
||
Example: " dws aitable dashboard share get --base-id BASE_ID --dashboard-id DASHBOARD_ID",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, dashboardID, err := requiredAitableDashboard(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runAitableTool(cmd, runner, "get_dashboard_share", map[string]any{
|
||
"baseId": baseID,
|
||
"dashboardId": dashboardID,
|
||
})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableDashboardIDFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newAitableDashboardShareUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "update",
|
||
Short: i18n.T("更新仪表盘分享配置"),
|
||
Example: " dws aitable dashboard share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --enabled true --share-type PUBLIC",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, dashboardID, err := requiredAitableDashboard(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params, err := aitableShareUpdateParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["baseId"] = baseID
|
||
params["dashboardId"] = dashboardID
|
||
return runAitableTool(cmd, runner, "update_dashboard_share", params)
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableDashboardIDFlags(cmd)
|
||
addAitableShareUpdateFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newAitableChartGetCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "get",
|
||
Short: i18n.T("获取图表详情"),
|
||
Example: " dws aitable chart get --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, dashboardID, chartID, err := requiredAitableChart(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runAitableTool(cmd, runner, "get_chart", map[string]any{
|
||
"baseId": baseID,
|
||
"dashboardId": dashboardID,
|
||
"chartId": chartID,
|
||
})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableChartIDFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newAitableChartCreateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "create",
|
||
Short: i18n.T("创建图表"),
|
||
Example: " dws aitable chart create --base-id BASE_ID --dashboard-id DASHBOARD_ID --config '{\"chartName\":\"销售柱图\"}' --layout '{\"x\":0,\"y\":0,\"w\":6,\"h\":4}'",
|
||
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
|
||
}
|
||
dashboardID, err := aitableRequiredFlag(cmd, "dashboard-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
configRaw, err := aitableRequiredFlag(cmd, "config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
layoutRaw, err := aitableRequiredFlag(cmd, "layout")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
configValue, err := parseAitableJSONObject(configRaw, "config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
layoutValue, err := parseAitableJSONObject(layoutRaw, "layout")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runAitableTool(cmd, runner, "create_chart", map[string]any{
|
||
"baseId": baseID,
|
||
"dashboardId": dashboardID,
|
||
"config": configValue,
|
||
"layout": layoutValue,
|
||
})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)"))
|
||
addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名")
|
||
cmd.Flags().String("dashboard-id", "", i18n.T("Dashboard ID (必填)"))
|
||
cmd.Flags().String("config", "", i18n.T("图表配置 JSON (必填)"))
|
||
cmd.Flags().String("layout", "", i18n.T("图表布局 JSON (必填)"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableChartUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "update",
|
||
Short: i18n.T("更新图表"),
|
||
Example: ` dws aitable chart update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID --config '{"chartName":"新图表"}'`,
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, dashboardID, chartID, err := requiredAitableChart(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"baseId": baseID,
|
||
"dashboardId": dashboardID,
|
||
"chartId": chartID,
|
||
}
|
||
if configRaw := aitableStringFlag(cmd, "config"); configRaw != "" {
|
||
config, err := parseAitableJSONObject(configRaw, "config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["config"] = config
|
||
}
|
||
if layoutRaw := aitableStringFlag(cmd, "layout"); layoutRaw != "" {
|
||
layout, err := parseAitableJSONObject(layoutRaw, "layout")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["layout"] = layout
|
||
}
|
||
if _, hasConfig := params["config"]; !hasConfig {
|
||
if _, hasLayout := params["layout"]; !hasLayout {
|
||
return apperrors.NewValidation("at least one of --config or --layout is required")
|
||
}
|
||
}
|
||
return runAitableTool(cmd, runner, "update_chart", params)
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableChartIDFlags(cmd)
|
||
cmd.Flags().String("config", "", i18n.T("图表配置 JSON"))
|
||
cmd.Flags().String("layout", "", i18n.T("图表布局 JSON"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableChartDeleteCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "delete",
|
||
Short: i18n.T("删除图表"),
|
||
Long: i18n.T("删除指定图表(高风险、不可逆)。使用 --yes 跳过确认。"),
|
||
Example: " dws aitable chart delete --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID --yes",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, dashboardID, chartID, err := requiredAitableChart(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !confirmDeletePrompt(cmd, i18n.T("图表"), chartID) {
|
||
return nil
|
||
}
|
||
params := map[string]any{
|
||
"baseId": baseID,
|
||
"dashboardId": dashboardID,
|
||
"chartId": chartID,
|
||
}
|
||
if reason := aitableStringFlag(cmd, "reason"); reason != "" {
|
||
params["reason"] = reason
|
||
}
|
||
return runAitableTool(cmd, runner, "delete_chart", params)
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableChartIDFlags(cmd)
|
||
cmd.Flags().String("reason", "", i18n.T("删除原因(可选)"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableChartWidgetsExampleCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "widgets-example",
|
||
Short: i18n.T("获取图表配置模板"),
|
||
Example: " dws aitable chart widgets-example",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
return runAitableTool(cmd, runner, "get_dashboard_widgets_example", map[string]any{})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newAitableChartShareGetCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "get",
|
||
Short: i18n.T("获取图表分享配置"),
|
||
Example: " dws aitable chart share get --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, dashboardID, chartID, err := requiredAitableChart(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runAitableTool(cmd, runner, "get_chart_share", map[string]any{
|
||
"baseId": baseID,
|
||
"dashboardId": dashboardID,
|
||
"chartId": chartID,
|
||
})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableChartIDFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newAitableChartShareUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "update",
|
||
Short: i18n.T("更新图表分享配置"),
|
||
Example: " dws aitable chart share update --base-id BASE_ID --dashboard-id DASHBOARD_ID --chart-id CHART_ID --enabled true --share-type PUBLIC",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, dashboardID, chartID, err := requiredAitableChart(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params, err := aitableShareUpdateParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["baseId"] = baseID
|
||
params["dashboardId"] = dashboardID
|
||
params["chartId"] = chartID
|
||
return runAitableTool(cmd, runner, "update_chart_share", params)
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableChartIDFlags(cmd)
|
||
addAitableShareUpdateFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
// ── view ───────────────────────────────────────────────────
|
||
|
||
func newAitableViewGetCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "get",
|
||
Short: i18n.T("获取视图配置"),
|
||
Example: " dws aitable view get --base-id BASE_ID --table-id TABLE_ID\n dws aitable view get --base-id BASE_ID --table-id TABLE_ID --view-ids view1,view2",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, tableID, err := requiredAitableBaseTable(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"baseId": baseID,
|
||
"tableId": tableID,
|
||
}
|
||
if viewIDs := aitableStringFlag(cmd, "view-ids"); viewIDs != "" {
|
||
params["viewIds"] = parseAitableCSVValues(viewIDs)
|
||
}
|
||
return runAitableTool(cmd, runner, "get_views", params)
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableBaseTableFlags(cmd)
|
||
cmd.Flags().String("view-ids", "", i18n.T("View ID 列表,逗号分隔;不传返回全部视图"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableViewListCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newAitableViewGetCommand(runner)
|
||
cmd.Use = "list"
|
||
cmd.Aliases = nil
|
||
cmd.Short = i18n.T("列出视图配置(view get 的别名)")
|
||
cmd.Example = " dws aitable view list --base-id BASE_ID --table-id TABLE_ID"
|
||
return cmd
|
||
}
|
||
|
||
func newAitableViewCreateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "create",
|
||
Short: i18n.T("创建视图"),
|
||
Example: " dws aitable view create --base-id BASE_ID --table-id TABLE_ID --view-type Grid --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
|
||
}
|
||
viewType, err := aitableRequiredFlag(cmd, "view-type")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"baseId": baseID,
|
||
"tableId": tableID,
|
||
"viewType": viewType,
|
||
}
|
||
if name := aitableStringFlag(cmd, "name"); name != "" {
|
||
params["viewName"] = name
|
||
}
|
||
if desc := aitableStringFlag(cmd, "desc"); desc != "" {
|
||
descValue, err := parseAitableJSONObject(desc, "desc")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["viewDescription"] = descValue
|
||
}
|
||
if viewSubType := aitableStringFlag(cmd, "view-sub-type"); viewSubType != "" {
|
||
params["viewSubType"] = viewSubType
|
||
}
|
||
if configRaw := aitableStringFlag(cmd, "config"); configRaw != "" {
|
||
configValue, err := parseAitableJSONObject(configRaw, "config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["config"] = configValue
|
||
}
|
||
return runAitableTool(cmd, runner, "create_view", params)
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)"))
|
||
addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名")
|
||
cmd.Flags().String("table-id", "", i18n.T("Table ID (必填)"))
|
||
cmd.Flags().String("view-type", "", i18n.T("视图类型 (必填)"))
|
||
cmd.Flags().String("name", "", i18n.T("视图名称"))
|
||
cmd.Flags().String("view-sub-type", "", i18n.T("视图子类型"))
|
||
cmd.Flags().String("desc", "", i18n.T("视图描述 JSON"))
|
||
cmd.Flags().String("config", "", i18n.T("视图配置 JSON"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableViewUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "update",
|
||
Short: i18n.T("更新视图"),
|
||
Example: ` dws aitable view update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --config '{"visibleFieldIds":["fld1","fld2"]}'`,
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, tableID, viewID, err := requiredAitableBaseTableView(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"baseId": baseID,
|
||
"tableId": tableID,
|
||
"viewId": viewID,
|
||
}
|
||
if name := aitableStringFlag(cmd, "name"); name != "" {
|
||
params["newViewName"] = name
|
||
}
|
||
if descRaw := aitableStringFlag(cmd, "desc"); descRaw != "" {
|
||
desc, err := parseAitableJSONObject(descRaw, "desc")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["viewDescription"] = desc
|
||
}
|
||
if configRaw := aitableStringFlag(cmd, "config"); configRaw != "" {
|
||
config, err := parseAitableJSONObject(configRaw, "config")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := normalizeAitableViewConfigBlock(cmd, config); err != nil {
|
||
return err
|
||
}
|
||
params["config"] = config
|
||
}
|
||
if _, hasName := params["newViewName"]; !hasName {
|
||
if _, hasDesc := params["viewDescription"]; !hasDesc {
|
||
if _, hasConfig := params["config"]; !hasConfig {
|
||
return apperrors.NewValidation("at least one of --name, --desc or --config is required")
|
||
}
|
||
}
|
||
}
|
||
return runAitableTool(cmd, runner, "update_view", params)
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableBaseTableViewFlags(cmd)
|
||
cmd.Flags().String("name", "", i18n.T("新视图名称"))
|
||
cmd.Flags().String("desc", "", i18n.T("视图描述 JSON"))
|
||
cmd.Flags().String("config", "", i18n.T("视图配置 JSON"))
|
||
return cmd
|
||
}
|
||
|
||
func newAitableViewDeleteCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "delete",
|
||
Short: i18n.T("删除视图"),
|
||
Long: i18n.T("删除指定视图(高风险、不可逆)。使用 --yes 跳过确认。"),
|
||
Example: " dws aitable view delete --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --yes",
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
baseID, tableID, viewID, err := requiredAitableBaseTableView(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !confirmDeletePrompt(cmd, i18n.T("视图"), viewID) {
|
||
return nil
|
||
}
|
||
return runAitableTool(cmd, runner, "delete_view", map[string]any{
|
||
"baseId": baseID,
|
||
"tableId": tableID,
|
||
"viewId": viewID,
|
||
})
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
addAitableBaseTableViewFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
// ── helpers ────────────────────────────────────────────────
|
||
|
||
func runAitableTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error {
|
||
return runAitableProductTool(cmd, runner, "aitable", tool, params)
|
||
}
|
||
|
||
func runAitableFormTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error {
|
||
return runAitableProductTool(cmd, runner, "aitable-form", tool, params)
|
||
}
|
||
|
||
func runAitableProductTool(cmd *cobra.Command, runner executor.Runner, product, tool string, params map[string]any) error {
|
||
result, err := runAitableProductToolResult(cmd, runner, product, tool, params)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return writeCommandPayload(cmd, result)
|
||
}
|
||
|
||
func runAitableProductToolResult(cmd *cobra.Command, runner executor.Runner, product, tool string, params map[string]any) (executor.Result, error) {
|
||
invocation := executor.NewHelperInvocation(
|
||
cobracmd.LegacyCommandPath(cmd),
|
||
product,
|
||
tool,
|
||
params,
|
||
)
|
||
invocation.DryRun = commandDryRun(cmd)
|
||
if invocation.DryRun {
|
||
result, err := runner.Run(cmd.Context(), invocation)
|
||
if err != nil {
|
||
return executor.Result{}, err
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
var lastErr error
|
||
for attempt := 0; attempt <= aitableHelperMaxRetries; attempt++ {
|
||
if attempt > 0 {
|
||
backoff := time.Duration(1<<(attempt-1)) * time.Second
|
||
fmt.Fprintf(cmd.ErrOrStderr(), "[%s retry %d/%d] %s after %v...\n", product, attempt, aitableHelperMaxRetries, tool, backoff)
|
||
timer := time.NewTimer(backoff)
|
||
select {
|
||
case <-cmd.Context().Done():
|
||
timer.Stop()
|
||
return executor.Result{}, cmd.Context().Err()
|
||
case <-timer.C:
|
||
}
|
||
}
|
||
|
||
result, err := runner.Run(cmd.Context(), invocation)
|
||
lastErr = err
|
||
if err == nil {
|
||
return result, nil
|
||
}
|
||
if !aitableErrorRetryable(err) {
|
||
return executor.Result{}, err
|
||
}
|
||
}
|
||
if lastErr != nil {
|
||
return executor.Result{}, lastErr
|
||
}
|
||
return executor.Result{}, nil
|
||
}
|
||
|
||
const aitableHelperMaxRetries = 3
|
||
|
||
func aitableErrorRetryable(err error) bool {
|
||
if err == nil {
|
||
return false
|
||
}
|
||
msg := strings.ToLower(err.Error())
|
||
|
||
retryablePatterns := []string{
|
||
"timeout", "deadline exceeded", "connection reset",
|
||
"connection refused", "broken pipe", "eof",
|
||
"network is unreachable", "i/o timeout",
|
||
"tls handshake", "server misbehaving", "temporary failure",
|
||
"no such host",
|
||
}
|
||
for _, pattern := range retryablePatterns {
|
||
if strings.Contains(msg, pattern) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
if strings.Contains(msg, "system_error") || strings.Contains(msg, "internal_error") ||
|
||
strings.Contains(msg, "service_unavailable") || strings.Contains(msg, "gateway_timeout") {
|
||
return true
|
||
}
|
||
|
||
if strings.Contains(msg, "retryable") && strings.Contains(msg, "true") {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
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 requiredAitableDashboard(cmd *cobra.Command) (string, string, error) {
|
||
baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base")
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
dashboardID, err := aitableRequiredFlag(cmd, "dashboard-id")
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
return baseID, dashboardID, nil
|
||
}
|
||
|
||
func requiredAitableChart(cmd *cobra.Command) (string, string, string, error) {
|
||
baseID, dashboardID, err := requiredAitableDashboard(cmd)
|
||
if err != nil {
|
||
return "", "", "", err
|
||
}
|
||
chartID, err := aitableRequiredFlag(cmd, "chart-id")
|
||
if err != nil {
|
||
return "", "", "", err
|
||
}
|
||
return baseID, dashboardID, chartID, nil
|
||
}
|
||
|
||
func addAitableHiddenStringFlag(cmd *cobra.Command, name, usage string) {
|
||
cmd.Flags().String(name, "", i18n.T(usage))
|
||
_ = cmd.Flags().MarkHidden(name)
|
||
}
|
||
|
||
func addAitableDashboardIDFlags(cmd *cobra.Command) {
|
||
cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)"))
|
||
addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名")
|
||
cmd.Flags().String("dashboard-id", "", i18n.T("Dashboard ID (必填)"))
|
||
}
|
||
|
||
func addAitableChartIDFlags(cmd *cobra.Command) {
|
||
addAitableDashboardIDFlags(cmd)
|
||
cmd.Flags().String("chart-id", "", i18n.T("Chart ID (必填)"))
|
||
}
|
||
|
||
func addAitableShareUpdateFlags(cmd *cobra.Command) {
|
||
cmd.Flags().String("enabled", "", i18n.T("是否开启分享 (必填): true/false"))
|
||
cmd.Flags().String("share-type", "", i18n.T("分享类型: PUBLIC / ORG"))
|
||
cmd.Flags().String("allow-back-to-doc", "", i18n.T("是否允许从分享页返回源文档: true/false"))
|
||
}
|
||
|
||
func aitableShareUpdateParams(cmd *cobra.Command) (map[string]any, error) {
|
||
enabledRaw := aitableStringFlag(cmd, "enabled")
|
||
if enabledRaw == "" {
|
||
return nil, apperrors.NewValidation("--enabled is required")
|
||
}
|
||
enabled, err := parseAitableBoolString(enabledRaw, "enabled")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
params := map[string]any{"enabled": enabled}
|
||
if shareType := aitableStringFlag(cmd, "share-type"); shareType != "" {
|
||
params["shareType"] = shareType
|
||
}
|
||
if allowRaw := aitableStringFlag(cmd, "allow-back-to-doc"); allowRaw != "" {
|
||
allow, err := parseAitableBoolString(allowRaw, "allow-back-to-doc")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
params["allowBackToDoc"] = allow
|
||
}
|
||
return params, nil
|
||
}
|
||
|
||
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 normalizeAitableFields(fields), nil
|
||
}
|
||
var wrapper map[string]any
|
||
if err := json.Unmarshal([]byte(raw), &wrapper); err == nil {
|
||
if wrappedFields, ok := wrapper["fields"].([]any); ok {
|
||
return normalizeAitableFields(wrappedFields), nil
|
||
}
|
||
}
|
||
return nil, apperrors.NewValidation("--fields JSON parse failed: expect a JSON array")
|
||
}
|
||
|
||
func normalizeAitableFields(fields []any) []any {
|
||
for _, field := range fields {
|
||
fieldMap, ok := field.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
if _, ok := fieldMap["fieldName"]; !ok {
|
||
if name, ok := fieldMap["name"].(string); ok && strings.TrimSpace(name) != "" {
|
||
fieldMap["fieldName"] = strings.TrimSpace(name)
|
||
delete(fieldMap, "name")
|
||
}
|
||
}
|
||
if fieldType, ok := fieldMap["type"].(string); ok {
|
||
fieldMap["type"] = normalizeAitableFieldType(fieldType)
|
||
}
|
||
}
|
||
return fields
|
||
}
|
||
|
||
func normalizeAitableFieldType(raw string) string {
|
||
trimmed := strings.TrimSpace(raw)
|
||
if trimmed == "" {
|
||
return trimmed
|
||
}
|
||
key := strings.ToLower(strings.NewReplacer("-", "", "_", "", " ", "").Replace(trimmed))
|
||
aliases := map[string]string{
|
||
"text": "text",
|
||
"number": "number",
|
||
"singleselect": "singleSelect",
|
||
"select": "singleSelect",
|
||
"multipleselect": "multipleSelect",
|
||
"multiselect": "multipleSelect",
|
||
"date": "date",
|
||
"currency": "currency",
|
||
"progress": "progress",
|
||
"rating": "rating",
|
||
"checkbox": "checkbox",
|
||
"user": "user",
|
||
"member": "user",
|
||
"person": "user",
|
||
"department": "department",
|
||
"group": "group",
|
||
"url": "url",
|
||
"richtext": "richText",
|
||
"telephone": "telephone",
|
||
"phone": "telephone",
|
||
"email": "email",
|
||
"attachment": "attachment",
|
||
"file": "attachment",
|
||
"geolocation": "geolocation",
|
||
"formula": "formula",
|
||
"unidirectionallink": "unidirectionalLink",
|
||
"bidirectionallink": "bidirectionalLink",
|
||
"creator": "creator",
|
||
"lastmodifier": "lastModifier",
|
||
"createdtime": "createdTime",
|
||
"lastmodifiedtime": "lastModifiedTime",
|
||
}
|
||
if normalized, ok := aliases[key]; ok {
|
||
return normalized
|
||
}
|
||
return trimmed
|
||
}
|
||
|
||
func validateAitableFiltersStructure(parsed map[string]any) error {
|
||
if parsed == nil {
|
||
return nil
|
||
}
|
||
op, hasOp := parsed["operator"]
|
||
if !hasOp {
|
||
return apperrors.NewValidation(`invalid --filters structure: missing "operator" field at root level`)
|
||
}
|
||
opStr, ok := op.(string)
|
||
if !ok {
|
||
return apperrors.NewValidation(fmt.Sprintf(`invalid --filters structure: "operator" must be a string, got %T`, op))
|
||
}
|
||
opLower := strings.ToLower(opStr)
|
||
if opLower != "and" && opLower != "or" {
|
||
return apperrors.NewValidation(fmt.Sprintf(`invalid --filters structure: root "operator" must be "and" or "or", got %q`, opStr))
|
||
}
|
||
operands, hasOperands := parsed["operands"]
|
||
if !hasOperands {
|
||
return apperrors.NewValidation(`invalid --filters structure: missing "operands" array at root level`)
|
||
}
|
||
operandsArr, ok := operands.([]any)
|
||
if !ok {
|
||
return apperrors.NewValidation(fmt.Sprintf(`invalid --filters structure: "operands" must be an array, got %T`, operands))
|
||
}
|
||
for i, item := range operandsArr {
|
||
cond, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
childOp, has := cond["operator"]
|
||
if !has {
|
||
continue
|
||
}
|
||
childOpStr, ok := childOp.(string)
|
||
if !ok {
|
||
continue
|
||
}
|
||
if suggestion := suggestAitableFilterOperator(childOpStr); suggestion != "" {
|
||
return apperrors.NewValidation(fmt.Sprintf(`invalid filter operator %q in operands[%d]. Did you mean %q?`, childOpStr, i, suggestion))
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
var aitableValidFilterOperators = map[string]bool{
|
||
"eq": true, "ne": true, "gt": true, "lt": true, "gte": true, "lte": true,
|
||
"contain": true, "exclusive": true, "exist": true, "un_exist": true,
|
||
"any_of": true, "all_of": true, "none_of": true,
|
||
"date_eq": true, "before": true, "after": true, "not_before": true, "not_after": true,
|
||
"from_now": true, "date_between": true,
|
||
"and": true, "or": true,
|
||
}
|
||
|
||
var aitableFilterOperatorAliases = map[string]string{
|
||
"equal": "eq", "equals": "eq", "is": "eq", "==": "eq",
|
||
"not_equal": "ne", "not_equals": "ne", "is_not": "ne", "!=": "ne",
|
||
"like": "contain", "contains": "contain", "include": "contain",
|
||
"greater_than": "gt", "less_than": "lt",
|
||
"greater_than_or_equal": "gte", "less_than_or_equal": "lte",
|
||
}
|
||
|
||
func suggestAitableFilterOperator(op string) string {
|
||
if aitableValidFilterOperators[op] {
|
||
return ""
|
||
}
|
||
if suggestion, ok := aitableFilterOperatorAliases[op]; ok {
|
||
return suggestion
|
||
}
|
||
return "eq"
|
||
}
|
||
|
||
func normalizeAitableFilters(parsed any) any {
|
||
filterMap, ok := parsed.(map[string]any)
|
||
if !ok {
|
||
return parsed
|
||
}
|
||
operands, has := filterMap["operands"]
|
||
if !has {
|
||
return parsed
|
||
}
|
||
operandsArr, ok := operands.([]any)
|
||
if !ok {
|
||
return parsed
|
||
}
|
||
normalized := make([]any, 0, len(operandsArr))
|
||
for _, item := range operandsArr {
|
||
cond, ok := item.(map[string]any)
|
||
if !ok {
|
||
normalized = append(normalized, item)
|
||
continue
|
||
}
|
||
if fieldID, hasFieldID := cond["fieldId"]; hasFieldID {
|
||
newCond := map[string]any{"operator": cond["operator"]}
|
||
if value, hasValue := cond["value"]; hasValue {
|
||
newCond["operands"] = []any{fieldID, value}
|
||
} else {
|
||
newCond["operands"] = []any{fieldID}
|
||
}
|
||
normalized = append(normalized, newCond)
|
||
continue
|
||
}
|
||
if _, hasChildOperands := cond["operands"]; hasChildOperands {
|
||
normalized = append(normalized, normalizeAitableFilters(cond))
|
||
continue
|
||
}
|
||
normalized = append(normalized, cond)
|
||
}
|
||
return map[string]any{
|
||
"operator": filterMap["operator"],
|
||
"operands": normalized,
|
||
}
|
||
}
|
||
|
||
func normalizeAitableSort(items []any) []any {
|
||
normalized := make([]any, 0, len(items))
|
||
for _, item := range items {
|
||
sortItem, ok := item.(map[string]any)
|
||
if !ok {
|
||
normalized = append(normalized, item)
|
||
continue
|
||
}
|
||
if val, hasOrder := sortItem["order"]; hasOrder {
|
||
if _, hasDirection := sortItem["direction"]; !hasDirection {
|
||
sortItem["direction"] = val
|
||
delete(sortItem, "order")
|
||
}
|
||
}
|
||
normalized = append(normalized, sortItem)
|
||
}
|
||
return normalized
|
||
}
|
||
|
||
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))
|
||
}
|
||
arr, ok := value.([]any)
|
||
if !ok {
|
||
return nil, apperrors.NewValidation(fmt.Sprintf("--%s must be a JSON array / 数组, got %T", flagName, value))
|
||
}
|
||
return arr, 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
|
||
}
|
||
|
||
// runRecordQueryAll 实现 dws aitable record query --all:用 pkg/paging 反复调
|
||
// query_records 直到拉完 / 触发 page-limit / 中途失败优雅降级。
|
||
//
|
||
// 输出契约(写到 stdout 的 JSON):
|
||
//
|
||
// {
|
||
// "records": [...累计所有页...],
|
||
// "hasMore": true|false,
|
||
// "cursor": "下一页 cursor,hasMore=true 时有效",
|
||
// "partial": true|false,
|
||
// "pages": 实际翻了多少页
|
||
// }
|
||
func runRecordQueryAll(cmd *cobra.Command, runner executor.Runner, baseParams map[string]any, initialCursor string) error {
|
||
pageLimit, _ := cmd.Flags().GetInt("page-limit")
|
||
|
||
// dry-run 模式:只打印第一次请求的预览,不实际循环
|
||
if commandDryRun(cmd) {
|
||
params := cloneMap(baseParams)
|
||
if initialCursor != "" {
|
||
params["cursor"] = initialCursor
|
||
}
|
||
params["__paging__"] = map[string]any{"all": true, "pageLimit": pageLimit}
|
||
return writeCommandPayload(cmd, executor.NewHelperInvocation(
|
||
cobracmd.LegacyCommandPath(cmd), "aitable", "query_records", params,
|
||
))
|
||
}
|
||
|
||
fetcher := func(ctx context.Context, cursor string) (paging.Page, error) {
|
||
params := cloneMap(baseParams)
|
||
if cursor != "" {
|
||
params["cursor"] = cursor
|
||
}
|
||
invocation := executor.NewHelperInvocation(
|
||
cobracmd.LegacyCommandPath(cmd), "aitable", "query_records", params,
|
||
)
|
||
result, err := runner.Run(ctx, invocation)
|
||
if err != nil {
|
||
return paging.Page{}, err
|
||
}
|
||
records, nextCursor := extractRecordsAndCursor(result.Response)
|
||
return paging.Page{Records: records, NextCursor: nextCursor}, nil
|
||
}
|
||
|
||
res := paging.FetchAll(cmd.Context(), fetcher, paging.Options{
|
||
PageLimit: pageLimit,
|
||
InitialCursor: initialCursor,
|
||
})
|
||
|
||
// 组装输出
|
||
output := map[string]any{
|
||
"records": res.Records,
|
||
"hasMore": res.HasMore,
|
||
"pages": res.Pages,
|
||
}
|
||
if res.LastCursor != "" {
|
||
output["cursor"] = res.LastCursor
|
||
}
|
||
if res.Partial {
|
||
output["partial"] = true
|
||
}
|
||
// 因 page-limit 截断不算错误,其它错误透传到 stderr 提示但不退出 1(保留已拉数据)
|
||
if res.Err != nil && !errors.Is(res.Err, paging.ErrPageLimitReached) {
|
||
fmt.Fprintf(cmd.ErrOrStderr(), "warning: %v (returned %d records before failure)\n", res.Err, len(res.Records))
|
||
}
|
||
|
||
return writeCommandPayload(cmd, output)
|
||
}
|
||
|
||
// extractRecordsAndCursor 从 query_records MCP 响应里抽出 records 数组与 nextCursor。
|
||
// 兼容两种包装:result.Response 直接含 records / 经 content/data 二次包装。
|
||
func extractRecordsAndCursor(resp map[string]any) ([]any, string) {
|
||
if resp == nil {
|
||
return nil, ""
|
||
}
|
||
src := resp
|
||
if content, ok := src["content"].(map[string]any); ok && len(content) > 0 {
|
||
src = content
|
||
}
|
||
data, _ := src["data"].(map[string]any)
|
||
if data == nil {
|
||
data = src
|
||
}
|
||
records, _ := data["records"].([]any)
|
||
cursor, _ := data["nextCursor"].(string)
|
||
if cursor == "" {
|
||
// 容错:部分上游可能用 cursor / pageToken 字段名
|
||
if c, ok := data["cursor"].(string); ok {
|
||
cursor = c
|
||
} else if c, ok := data["pageToken"].(string); ok {
|
||
cursor = c
|
||
}
|
||
}
|
||
return records, cursor
|
||
}
|
||
|
||
func resolveAitableRecordCreateRecords(cmd *cobra.Command) ([]any, error) {
|
||
return resolveAitableRecordsInput(cmd, "records", "fields")
|
||
}
|
||
|
||
func resolveAitableRecordUpdateRecords(cmd *cobra.Command) ([]any, error) {
|
||
if records, err := resolveAitableRecordsInput(cmd, "records", "fields"); err == nil {
|
||
return records, nil
|
||
} else if aitableStringFlag(cmd, "records-file") != "" ||
|
||
aitableStringFlag(cmd, "records") != "" ||
|
||
aitableStringFlag(cmd, "fields") != "" {
|
||
return nil, err
|
||
}
|
||
|
||
recordID := aitableStringFlag(cmd, "record-id")
|
||
if dataRaw := aitableStringFlag(cmd, "data"); dataRaw != "" {
|
||
return parseAitableRecordUpdateData(dataRaw, recordID)
|
||
}
|
||
|
||
cellsRaw := aitableStringFlag(cmd, "cells")
|
||
if recordID != "" || cellsRaw != "" {
|
||
if recordID == "" {
|
||
return nil, apperrors.NewValidation("--record-id is required when using --cells")
|
||
}
|
||
if cellsRaw == "" {
|
||
return nil, apperrors.NewValidation("--cells is required when using --record-id")
|
||
}
|
||
cells, err := parseAitableJSONObject(cellsRaw, "cells")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return []any{map[string]any{
|
||
"recordId": recordID,
|
||
"cells": cells,
|
||
}}, nil
|
||
}
|
||
|
||
return nil, apperrors.NewValidation("--records is required")
|
||
}
|
||
|
||
func resolveAitableRecordsInput(cmd *cobra.Command, primary string, aliases ...string) ([]any, error) {
|
||
if filePath := aitableStringFlag(cmd, "records-file"); filePath != "" {
|
||
return parseAitableRecordsFile(filePath)
|
||
}
|
||
if recordsRaw := aitableStringFlag(cmd, primary); recordsRaw != "" {
|
||
return parseAitableJSONArray(recordsRaw, primary)
|
||
}
|
||
for _, alias := range aliases {
|
||
if recordsRaw := aitableStringFlag(cmd, alias); recordsRaw != "" {
|
||
return parseAitableJSONArray(recordsRaw, alias)
|
||
}
|
||
}
|
||
return nil, apperrors.NewValidation("--records is required")
|
||
}
|
||
|
||
func parseAitableRecordsFile(filePath string) ([]any, error) {
|
||
data, err := os.ReadFile(filePath)
|
||
if err != nil {
|
||
return nil, apperrors.NewValidation(fmt.Sprintf("--records-file read failed: %v", err))
|
||
}
|
||
raw := strings.TrimSpace(string(data))
|
||
if raw == "" {
|
||
return nil, apperrors.NewValidation("--records-file is empty")
|
||
}
|
||
return parseAitableJSONArray(raw, "records-file")
|
||
}
|
||
|
||
func parseAitableRecordUpdateData(raw, recordID string) ([]any, error) {
|
||
var value any
|
||
if err := json.Unmarshal([]byte(raw), &value); err != nil {
|
||
return nil, apperrors.NewValidation(fmt.Sprintf("--data JSON parse failed: %v", err))
|
||
}
|
||
|
||
switch typed := value.(type) {
|
||
case []any:
|
||
return typed, nil
|
||
case map[string]any:
|
||
if recordsValue, ok := typed["records"]; ok {
|
||
records, ok := recordsValue.([]any)
|
||
if !ok {
|
||
return nil, apperrors.NewValidation("--data.records must be a JSON array")
|
||
}
|
||
return records, nil
|
||
}
|
||
|
||
if _, ok := typed["cells"]; ok {
|
||
if strings.TrimSpace(recordID) != "" && strings.TrimSpace(recordIDFromRecordObject(typed)) == "" {
|
||
typed["recordId"] = strings.TrimSpace(recordID)
|
||
}
|
||
if strings.TrimSpace(recordIDFromRecordObject(typed)) == "" {
|
||
return nil, apperrors.NewValidation("--data record object requires recordId")
|
||
}
|
||
return []any{typed}, nil
|
||
}
|
||
|
||
if strings.TrimSpace(recordID) != "" {
|
||
return []any{map[string]any{
|
||
"recordId": strings.TrimSpace(recordID),
|
||
"cells": typed,
|
||
}}, nil
|
||
}
|
||
|
||
return nil, apperrors.NewValidation("--record-id is required when --data is a cells object")
|
||
default:
|
||
return nil, apperrors.NewValidation("--data must be a JSON array or object")
|
||
}
|
||
}
|
||
|
||
func recordIDFromRecordObject(record map[string]any) string {
|
||
if record == nil {
|
||
return ""
|
||
}
|
||
if value, ok := record["recordId"].(string); ok {
|
||
return value
|
||
}
|
||
return ""
|
||
}
|