223 lines
8.0 KiB
Go
223 lines
8.0 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 (
|
||
"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 <NODE_ID> --sheet-id <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")
|
||
}
|
||
}
|