// 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" "io" "os" "strings" "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) func newTableCmds() []*cobra.Command { tableGetCmd := &cobra.Command{ Use: "table-get", Aliases: []string{"table-read"}, Short: "读取结构化 table 数据", Long: `读取结构化 table 数据。 返回列名、行数据、pandas-style dtypes 和表格 number formats。`, Example: ` dws sheet table-get --node NODE_ID dws sheet table-get --node NODE_ID --sheet-id SHEET_ID --range A1:D20 dws sheet table-get --node NODE_ID --sheet-id Sheet1 --no-header`, RunE: func(cmd *cobra.Command, args []string) error { nodeID, err := mustFlagOrFallback(cmd, "node", "file-id", "node-id", "doc-id") if err != nil { return err } toolArgs := map[string]any{ "nodeId": nodeID, } if v, _ := cmd.Flags().GetString("sheet-id"); v != "" { toolArgs["sheetId"] = v } if v, _ := cmd.Flags().GetString("range"); v != "" { toolArgs["range"] = v } if cmd.Flags().Changed("no-header") { v, _ := cmd.Flags().GetBool("no-header") toolArgs["noHeader"] = v } return callMCPTool("table_get", toolArgs) }, } DeclareLeafMetadata(tableGetCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "sheet", Name: "table_get", CanonicalPath: "sheet.table_get", CLIPath: "sheet table-get", PrimaryCLIPath: "sheet table-get", Aliases: []string{"sheet table-read"}, }, Description: "读取结构化 table 区域数据。", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.", }, Selection: contract.SelectionSpec{ AgentSummary: "读取结构化 table 区域数据。", UseWhen: []string{"工作表内存在结构化 table,需要按 table 语义读取时"}, AvoidWhen: []string{"普通单元格区域用 range read;AI 表格记录用 aitable record query"}, Examples: []string{"dws sheet table-get --node --sheet-id "}, }, Parameters: []contract.ParamDecl{ {Name: "node", Property: "nodeId"}, }, }, }) tableGetCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)") tableGetCmd.Flags().String("sheet-id", "", "工作表 ID 或名称") tableGetCmd.Flags().String("range", "", "读取范围,A1 表示法;可带 sheet 前缀,如 Sheet1!A1:D10") tableGetCmd.Flags().Bool("no-header", false, "首行不作为表头,自动生成 col1/col2/...") tablePutCmd := &cobra.Command{ Use: "table-put", Aliases: []string{"table-write"}, Short: "写入结构化 table 数据", Long: `写入结构化 table 数据。 支持一次写入一个或多个 sheet;目标 sheet 存在时写入,传 name 且不存在时会创建 sheet。 --sheets 支持三种输入: JSON 数组 [{"name":"Sheet1","columns":["name"],"data":[["Alice"]]}] JSON 对象 {"sheets":[{"name":"Sheet1","columns":["name"],"data":[["Alice"]]}]} 单个 sheet spec JSON 对象 {"name":"Sheet1","columns":["name"],"data":[["Alice"]]} --sheets 可传 @filepath 从文件读取,或传 - 从 stdin 读取。`, Example: ` dws sheet table-put --node NODE_ID \ --sheets '[{"name":"Sheet1","columns":["name","score"],"data":[["Alice",95]],"dtypes":{"score":"float64"}}]' dws sheet table-put --node NODE_ID --sheets @table.json cat table.json | dws sheet table-put --node NODE_ID --sheets -`, RunE: func(cmd *cobra.Command, args []string) error { nodeID, err := mustFlagOrFallback(cmd, "node", "file-id", "node-id", "doc-id") if err != nil { return err } if err := validateRequiredFlags(cmd, "sheets"); err != nil { return err } raw, err := readTableJSONFlag(cmd, "sheets") if err != nil { return err } sheets, err := parseTablePutSheets(raw) if err != nil { return err } return callMCPTool("table_put", map[string]any{ "nodeId": nodeID, "sheets": sheets, }) }, } DeclareLeafMetadata(tablePutCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ ProductID: "sheet", Name: "table_put", CanonicalPath: "sheet.table_put", CLIPath: "sheet table-put", PrimaryCLIPath: "sheet table-put", Aliases: []string{"sheet table-write"}, }, Description: "写入一个或多个结构化 table。", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.", }, Selection: contract.SelectionSpec{ AgentSummary: "写入一个或多个结构化 table。", UseWhen: []string{"需要按结构化 table 协议写入表数据时"}, AvoidWhen: []string{"普通区域写入用 range update/csv-put;AI 表格记录写入用 aitable record create"}, Examples: []string{"dws sheet table-put --node NODE_ID --sheets '[{\"name\":\"Sheet1\",\"columns\":[\"name\"],\"data\":[[\"Alice\"]]}]'"}, }, Parameters: []contract.ParamDecl{ {Name: "sheets", InterfaceType: "array"}, {Name: "node", Property: "nodeId"}, }, }, }) tablePutCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)") tablePutCmd.Flags().String("sheets", "", "sheet table JSON、@文件路径 或 - 表示 stdin (必填)") return []*cobra.Command{tableGetCmd, tablePutCmd} } func readTableJSONFlag(cmd *cobra.Command, flagName string) (string, error) { value := mustGetFlag(cmd, flagName) switch { case value == "-": data, err := io.ReadAll(cmd.InOrStdin()) if err != nil { return "", fmt.Errorf("读取 stdin 失败: %w", err) } return string(data), nil case strings.HasPrefix(value, "@"): data, err := os.ReadFile(strings.TrimPrefix(value, "@")) if err != nil { return "", fmt.Errorf("读取 JSON 文件失败: %w", err) } return string(data), nil default: return value, nil } } func parseTablePutSheets(raw string) ([]any, error) { var payload any if err := json.Unmarshal([]byte(raw), &payload); err != nil { return nil, fmt.Errorf("--sheets JSON 解析失败: %w", err) } switch value := payload.(type) { case []any: if len(value) == 0 { return nil, fmt.Errorf("--sheets must contain at least one sheet spec") } return value, nil case map[string]any: if sheets, ok := value["sheets"]; ok { items, ok := sheets.([]any) if !ok { return nil, fmt.Errorf("--sheets.sheets must be a JSON array") } if len(items) == 0 { return nil, fmt.Errorf("--sheets.sheets must contain at least one sheet spec") } return items, nil } return []any{value}, nil default: return nil, fmt.Errorf("--sheets must be a JSON array, an object with sheets, or a single sheet spec object") } }