Files
dingtalk-workspace-cli/internal/helpers/sheet_pivot_table.go

376 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
var validPivotSummarizeBy = map[string]bool{
"sum": true, "count": true, "average": true,
"max": true, "min": true, "product": true,
"count_numbers": true, "std_dev": true, "std_dev_p": true,
"var": true, "var_p": true, "distinct": true, "median": true,
}
func validatePivotTableProperties(props map[string]any, requireValues bool) error {
valuesRaw, hasValues := props["values"]
if requireValues && (!hasValues || valuesRaw == nil) {
return fmt.Errorf("--properties 缺少必填字段 values")
}
if hasValues && valuesRaw != nil {
values, ok := valuesRaw.([]any)
if !ok {
return fmt.Errorf("values 必须为数组")
}
if requireValues && len(values) == 0 {
return fmt.Errorf("values 必须至少包含一项")
}
for i, item := range values {
field, err := validatePivotField(item, fmt.Sprintf("values[%d]", i))
if err != nil {
return err
}
_ = field
obj := item.(map[string]any)
if raw, ok := obj["summarize_by"]; ok && raw != nil {
value, ok := raw.(string)
if !ok {
return fmt.Errorf("values[%d].summarize_by 必须为字符串", i)
}
normalized := strings.ToLower(strings.TrimSpace(value))
if !validPivotSummarizeBy[normalized] {
return fmt.Errorf("values[%d].summarize_by 不支持: %q", i, value)
}
obj["summarize_by"] = normalized
}
}
}
for _, name := range []string{"rows", "columns", "filters"} {
if err := validatePivotFieldArray(props, name); err != nil {
return err
}
}
if raw, ok := props["collapse"]; ok && raw != nil {
switch raw.(type) {
case map[string]any, []any:
default:
return fmt.Errorf("collapse 必须为对象或数组")
}
}
return nil
}
func validatePivotField(item any, path string) (string, error) {
obj, ok := item.(map[string]any)
if !ok {
return "", fmt.Errorf("%s 必须为对象", path)
}
raw, ok := obj["field"]
if !ok || raw == nil {
return "", fmt.Errorf("%s.field 为必填字段", path)
}
field, ok := raw.(string)
if !ok || strings.TrimSpace(field) == "" {
return "", fmt.Errorf("%s.field 必须为非空字符串", path)
}
return field, nil
}
func validatePivotFieldArray(props map[string]any, name string) error {
raw, ok := props[name]
if !ok || raw == nil {
return nil
}
items, ok := raw.([]any)
if !ok {
return fmt.Errorf("%s 必须为数组", name)
}
for i, item := range items {
if _, err := validatePivotField(item, fmt.Sprintf("%s[%d]", name, i)); err != nil {
return err
}
}
return nil
}
func readPivotProperties(raw string, requireValues bool) (map[string]any, error) {
if strings.HasPrefix(raw, "@") {
data, err := os.ReadFile(strings.TrimPrefix(raw, "@"))
if err != nil {
return nil, fmt.Errorf("读取 properties 文件失败: %w", err)
}
raw = string(data)
}
var properties map[string]any
if err := json.Unmarshal([]byte(raw), &properties); err != nil {
return nil, fmt.Errorf("--properties JSON 解析失败: %w", err)
}
if len(properties) == 0 {
return nil, fmt.Errorf("--properties 不能为空对象")
}
if err := validatePivotTableProperties(properties, requireValues); err != nil {
return nil, err
}
return properties, nil
}
func newPivotTableCmd() *cobra.Command {
root := &cobra.Command{
Use: "pivot-table",
Short: "透视表管理",
RunE: groupRunE,
}
listCmd := &cobra.Command{
Use: "list",
Short: "获取透视表列表或详情",
Example: ` dws sheet pivot-table list --node NODE_ID --sheet-id SHEET_ID
dws sheet pivot-table list --node NODE_ID --sheet-id SHEET_ID --pivot-table-id PT_ID`,
RunE: func(cmd *cobra.Command, _ []string) error {
if err := validateRequiredFlags(cmd, "node", "sheet-id"); err != nil {
return err
}
args := map[string]any{
"nodeId": mustGetFlag(cmd, "node"),
"sheetId": mustGetFlag(cmd, "sheet-id"),
}
if value, _ := cmd.Flags().GetString("pivot-table-id"); value != "" {
args["pivotTableId"] = value
}
return callMCPTool("list_pivot_tables", args)
},
}
DeclareLeafMetadata(listCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "sheet",
Name: "list_pivot_tables",
CanonicalPath: "sheet.list_pivot_tables",
CLIPath: "sheet pivot-table list",
PrimaryCLIPath: "sheet pivot-table list",
},
Description: "列出或获取透视表信息。",
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: "列出或获取透视表信息。",
UseWhen: []string{"需要查看工作表上有哪些透视表或某个透视表详情时"},
AvoidWhen: []string{"创建/更新/删除分别用 pivot-table create/update/delete"},
Examples: []string{"dws sheet pivot-table list --node NODE_ID --sheet-id SHEET_ID"},
},
Parameters: []contract.ParamDecl{
{Name: "node", Property: "nodeId"},
},
},
})
listCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
listCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
listCmd.Flags().String("pivot-table-id", "", "透视表 ID (可选,不传则返回全部)")
createCmd := &cobra.Command{
Use: "create",
Short: "创建透视表",
Long: `在指定数据源区域上创建原生透视表。
--source 必须使用带工作表前缀的 A1 范围,例如 "'Sheet1'!A1:D100"。
--properties 为 JSON 对象或 @file,values 至少一项;可包含 rows、columns、filters、collapse 和总计显示选项。`,
Example: ` dws sheet pivot-table create --node NODE_ID \
--source "'Sheet1'!A1:D100" \
--properties '{"rows":[{"field":"部门"}],"values":[{"field":"销售额","summarize_by":"sum"}]}'`,
RunE: func(cmd *cobra.Command, _ []string) error {
if err := validateRequiredFlags(cmd, "node", "source", "properties"); err != nil {
return err
}
properties, err := readPivotProperties(mustGetFlag(cmd, "properties"), true)
if err != nil {
return err
}
args := map[string]any{
"nodeId": mustGetFlag(cmd, "node"),
"source": mustGetFlag(cmd, "source"),
"properties": properties,
}
if value, _ := cmd.Flags().GetString("target-sheet-id"); value != "" {
args["targetSheetId"] = value
}
if value, _ := cmd.Flags().GetString("target-position"); value != "" {
args["targetPosition"] = value
}
return callMCPTool("create_pivot_table", args)
},
}
DeclareLeafMetadata(createCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "non_idempotent",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "sheet",
Name: "create_pivot_table",
CanonicalPath: "sheet.create_pivot_table",
CLIPath: "sheet pivot-table create",
PrimaryCLIPath: "sheet pivot-table create",
},
Description: "创建原生透视表。",
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: "创建原生透视表。",
UseWhen: []string{"需要基于源数据区域创建透视表时"},
AvoidWhen: []string{"更新配置用 update;删除用 delete"},
Examples: []string{"dws sheet pivot-table create --node NODE_ID --source \"'Sheet1'!A1:D100\" --properties '{\"rows\":[{\"field\":\"A\"}],\"values\":[{\"field\":\"D\",\"aggregation\":\"sum\"}]}'"},
},
Parameters: []contract.ParamDecl{
{Name: "properties", InterfaceType: "object"},
{Name: "node", Property: "nodeId"},
},
},
})
createCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
createCmd.Flags().String("source", "", "数据源区域,A1 表示法且包含工作表前缀 (必填)")
createCmd.Flags().String("properties", "", "透视表配置 JSON 或 @文件路径 (必填)")
createCmd.Flags().String("target-sheet-id", "", "目标工作表 ID 或名称 (可选,不传则自动新建)")
createCmd.Flags().String("target-position", "", "透视表放置位置,A1 单元格地址 (可选)")
updateCmd := &cobra.Command{
Use: "update",
Short: "更新透视表配置",
Example: ` dws sheet pivot-table update --node NODE_ID --sheet-id SHEET_ID \
--pivot-table-id PT_ID --properties '{"show_subtotals":false}'`,
RunE: func(cmd *cobra.Command, _ []string) error {
if err := validateRequiredFlags(cmd, "node", "sheet-id", "pivot-table-id", "properties"); err != nil {
return err
}
properties, err := readPivotProperties(mustGetFlag(cmd, "properties"), false)
if err != nil {
return err
}
return callMCPTool("update_pivot_table", map[string]any{
"nodeId": mustGetFlag(cmd, "node"),
"sheetId": mustGetFlag(cmd, "sheet-id"),
"pivotTableId": mustGetFlag(cmd, "pivot-table-id"),
"properties": properties,
})
},
}
DeclareLeafMetadata(updateCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "sheet",
Name: "update_pivot_table",
CanonicalPath: "sheet.update_pivot_table",
CLIPath: "sheet pivot-table update",
PrimaryCLIPath: "sheet pivot-table update",
},
Description: "更新透视表配置。",
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: "更新透视表配置。",
UseWhen: []string{"已知透视表 ID,需要调整行/列/值字段配置时"},
AvoidWhen: []string{"新建用 create;删除用 delete"},
Examples: []string{"dws sheet pivot-table update --node NODE_ID --sheet-id SHEET_ID --pivot-table-id PIVOT_TABLE_ID --properties '{\"show_subtotals\":false}'"},
},
Parameters: []contract.ParamDecl{
{Name: "properties", InterfaceType: "object"},
{Name: "node", Property: "nodeId"},
},
},
})
updateCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
updateCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
updateCmd.Flags().String("pivot-table-id", "", "透视表 ID (必填)")
updateCmd.Flags().String("properties", "", "需要更新的透视表配置 JSON 或 @文件路径 (必填)")
deleteCmd := &cobra.Command{
Use: "delete",
Short: "[危险] 删除透视表",
Example: ` dws sheet pivot-table delete --node NODE_ID --sheet-id SHEET_ID --pivot-table-id PT_ID --yes`,
RunE: func(cmd *cobra.Command, _ []string) error {
if err := validateRequiredFlags(cmd, "node", "sheet-id", "pivot-table-id"); err != nil {
return err
}
pivotTableID := mustGetFlag(cmd, "pivot-table-id")
return callMCPTool("delete_pivot_table", map[string]any{
"nodeId": mustGetFlag(cmd, "node"),
"sheetId": mustGetFlag(cmd, "sheet-id"),
"pivotTableId": pivotTableID,
})
},
}
DeclareLeafMetadata(deleteCmd, LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "user_required", Idempotency: "unknown",
},
Contract: LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "sheet",
Name: "delete_pivot_table",
CanonicalPath: "sheet.delete_pivot_table",
CLIPath: "sheet pivot-table delete",
PrimaryCLIPath: "sheet pivot-table delete",
},
Description: "删除透视表(需确认后加 --yes)。",
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: "删除透视表(需确认后加 --yes)。",
UseWhen: []string{"用户明确要求永久删除指定透视表时"},
AvoidWhen: []string{"只更新配置用 update"},
Examples: []string{"dws sheet pivot-table delete --node NODE_ID --sheet-id SHEET_ID --pivot-table-id PT_ID"},
},
Parameters: []contract.ParamDecl{
{Name: "node", Property: "nodeId"},
},
},
})
deleteCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
deleteCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
deleteCmd.Flags().String("pivot-table-id", "", "透视表 ID (必填)")
root.AddCommand(listCmd, createCmd, updateCmd, deleteCmd)
attachUnknownSubcommandGuard(root)
return root
}