- accept scalar cells in range update - flat values projection on read; null clears a cell - add --hyperlinks flag to range update
2371 lines
80 KiB
Go
2371 lines
80 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"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||
"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/spf13/cobra"
|
||
)
|
||
|
||
func init() {
|
||
RegisterPublic(func() Handler {
|
||
return sheetHandler{}
|
||
})
|
||
}
|
||
|
||
type sheetHandler struct{}
|
||
|
||
func (sheetHandler) Name() string {
|
||
return "sheet"
|
||
}
|
||
|
||
func (sheetHandler) Command(runner executor.Runner) *cobra.Command {
|
||
root := newSheetGroup("sheet", "钉钉电子表格")
|
||
root.Long = "管理钉钉在线电子表格:文档与工作表管理、单元格读写、CSV、行列、样式、筛选、条件格式和图片。"
|
||
|
||
rangeCmd := newSheetGroup("range", "数据区域操作")
|
||
rangeCmd.AddCommand(
|
||
newSheetRangeReadCommand(runner),
|
||
newSheetRangeUpdateCommand(runner),
|
||
newSheetRangeClearCommand(runner),
|
||
newSheetRangeSortCommand(runner),
|
||
newSheetRangeFillCommand(runner),
|
||
newSheetRangeCopyCommand(runner),
|
||
newSheetRangeMoveCommand(runner),
|
||
newSheetRangeSetStyleCommand(runner),
|
||
newSheetRangeBatchSetStyleCommand(runner),
|
||
)
|
||
|
||
filterCmd := newSheetGroup("filter", "全局筛选管理")
|
||
filterCmd.AddCommand(
|
||
newSheetFilterGetCommand(runner),
|
||
newSheetFilterCreateCommand(runner),
|
||
newSheetFilterDeleteCommand(runner),
|
||
newSheetFilterUpdateCommand(runner),
|
||
newSheetFilterClearCriteriaCommand(runner),
|
||
newSheetFilterSortCommand(runner),
|
||
)
|
||
|
||
filterViewCmd := newSheetGroup("filter-view", "筛选视图管理")
|
||
filterViewCmd.AddCommand(
|
||
newSheetFilterViewListCommand(runner),
|
||
newSheetFilterViewCreateCommand(runner),
|
||
newSheetFilterViewUpdateCommand(runner),
|
||
newSheetFilterViewDeleteCommand(runner),
|
||
newSheetFilterViewUpdateCriteriaCommand(runner),
|
||
newSheetFilterViewDeleteCriteriaCommand(runner),
|
||
newSheetFilterViewInfoCommand(runner),
|
||
newSheetFilterViewListCriteriaCommand(runner),
|
||
newSheetFilterViewGetCriteriaCommand(runner),
|
||
)
|
||
|
||
condFormatCmd := newSheetGroup("cond-format", "条件格式管理")
|
||
condFormatCmd.AddCommand(
|
||
newSheetCondFormatListCommand(runner),
|
||
newSheetCondFormatCreateCommand(runner),
|
||
newSheetCondFormatUpdateCommand(runner),
|
||
newSheetCondFormatDeleteCommand(runner),
|
||
)
|
||
|
||
root.AddCommand(
|
||
newSheetCreateCommand(runner),
|
||
newSheetListCommand(runner),
|
||
newSheetInfoCommand(runner),
|
||
newSheetNewCommand(runner),
|
||
newSheetUpdateCommand(runner),
|
||
newSheetCopyCommand(runner),
|
||
newSheetDeleteSheetCommand(runner),
|
||
rangeCmd,
|
||
newSheetFindCommand(runner),
|
||
newSheetAppendCommand(runner),
|
||
newSheetCSVPutCommand(runner),
|
||
newSheetCSVGetCommand(runner),
|
||
newSheetInsertDimensionCommand(runner),
|
||
newSheetDeleteDimensionCommand(runner),
|
||
newSheetUpdateDimensionCommand(runner),
|
||
newSheetMoveDimensionCommand(runner),
|
||
newSheetAddDimensionCommand(runner),
|
||
newSheetMergeCellsCommand(runner),
|
||
newSheetUnmergeCellsCommand(runner),
|
||
newSheetSetDropdownCommand(runner),
|
||
newSheetGetDropdownCommand(runner),
|
||
newSheetDeleteDropdownCommand(runner),
|
||
newSheetMediaUploadCommand(runner),
|
||
newSheetWriteImageCommand(runner),
|
||
newSheetReplaceCommand(runner),
|
||
filterCmd,
|
||
filterViewCmd,
|
||
condFormatCmd,
|
||
newSheetCreateFloatImageCommand(runner),
|
||
newSheetGetFloatImageCommand(runner),
|
||
newSheetListFloatImagesCommand(runner),
|
||
newSheetUpdateFloatImageCommand(runner),
|
||
newSheetDeleteFloatImageCommand(runner),
|
||
newSheetExportCommand(runner),
|
||
)
|
||
return root
|
||
}
|
||
|
||
func newSheetCreateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("create", "创建钉钉表格文档", func(cmd *cobra.Command, _ []string) error {
|
||
name, err := sheetRequiredFlag(cmd, "name")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{"name": name}
|
||
sheetAddStringParam(cmd, params, "folderId", "folder", "parent-id")
|
||
sheetAddStringParam(cmd, params, "workspaceId", "workspace", "workspace-id")
|
||
return runSheetTool(cmd, runner, "create_workspace_sheet", params)
|
||
})
|
||
cmd.Flags().String("name", "", "表格名称 (必填)")
|
||
cmd.Flags().String("folder", "", "目标文件夹 ID 或 URL")
|
||
cmd.Flags().String("workspace", "", "目标知识库 ID")
|
||
addSheetHiddenStringFlag(cmd, "workspace-id", "--workspace alias")
|
||
addSheetHiddenStringFlag(cmd, "parent-id", "--folder 的跨产品别名")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetListCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("list", "获取全部工作表列表", func(cmd *cobra.Command, _ []string) error {
|
||
node, err := sheetRequiredFlag(cmd, "node")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "get_all_sheets", map[string]any{"nodeId": node})
|
||
})
|
||
addSheetNodeFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newSheetInfoCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("info", "获取指定工作表详情", func(cmd *cobra.Command, _ []string) error {
|
||
node, err := sheetRequiredFlag(cmd, "node")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{"nodeId": node}
|
||
sheetAddStringParam(cmd, params, "sheetId", "sheet-id")
|
||
result, err := sheetInvocationResult(cmd, runner, "sheet", "get_sheet", params)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// 只向 agent 暴露 A1/UI 语义的 nonEmptyRange,清掉 legacy 0-based 字段(与 wukong 对齐)。
|
||
if !commandDryRun(cmd) {
|
||
sheetNormalizeInfoCoordinates(result.Response)
|
||
}
|
||
return writeCommandPayload(cmd, result)
|
||
})
|
||
addSheetNodeFlags(cmd)
|
||
cmd.Flags().String("sheet-id", "", "工作表 ID 或名称")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetNewCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("new", "新建工作表", func(cmd *cobra.Command, _ []string) error {
|
||
node, name, err := sheetNodeAndName(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "create_sheet", map[string]any{"nodeId": node, "name": name})
|
||
})
|
||
addSheetNodeFlags(cmd)
|
||
cmd.Flags().String("name", "", "工作表名称 (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("update", "更新工作表属性", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
changed := false
|
||
if cmd.Flags().Changed("name") || cmd.Flags().Changed("title") {
|
||
params["title"] = sheetNameFlag(cmd)
|
||
changed = true
|
||
}
|
||
if cmd.Flags().Changed("index") {
|
||
index, _ := cmd.Flags().GetInt("index")
|
||
if index < 0 {
|
||
return apperrors.NewValidation("--index must be >= 0")
|
||
}
|
||
params["index"] = index
|
||
changed = true
|
||
}
|
||
if cmd.Flags().Changed("hidden") {
|
||
v, _ := cmd.Flags().GetBool("hidden")
|
||
params["hidden"] = v
|
||
changed = true
|
||
}
|
||
if cmd.Flags().Changed("frozen-row-count") {
|
||
v, _ := cmd.Flags().GetInt("frozen-row-count")
|
||
if v < 0 {
|
||
return apperrors.NewValidation("--frozen-row-count must be >= 0")
|
||
}
|
||
params["frozenRowCount"] = v
|
||
changed = true
|
||
}
|
||
if cmd.Flags().Changed("frozen-column-count") {
|
||
v, _ := cmd.Flags().GetInt("frozen-column-count")
|
||
if v < 0 {
|
||
return apperrors.NewValidation("--frozen-column-count must be >= 0")
|
||
}
|
||
params["frozenColumnCount"] = v
|
||
changed = true
|
||
}
|
||
// tab-color:Hex 颜色(如 #FF0000),传空字符串清除颜色(与 wukong 对齐)。
|
||
if cmd.Flags().Changed("tab-color") {
|
||
v, _ := cmd.Flags().GetString("tab-color")
|
||
params["tabColor"] = v
|
||
changed = true
|
||
}
|
||
if !changed {
|
||
return apperrors.NewValidation("at least one of --name, --index, --hidden, --frozen-row-count, --frozen-column-count or --tab-color is required")
|
||
}
|
||
return runSheetTool(cmd, runner, "update_sheet", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("name", "", "工作表新名称")
|
||
cmd.Flags().String("title", "", "--name alias")
|
||
_ = cmd.Flags().MarkHidden("title")
|
||
cmd.Flags().Int("index", 0, "工作表新位置索引,0-based")
|
||
cmd.Flags().Bool("hidden", false, "是否隐藏工作表")
|
||
cmd.Flags().Int("frozen-row-count", 0, "冻结行数")
|
||
cmd.Flags().Int("frozen-column-count", 0, "冻结列数")
|
||
cmd.Flags().String("tab-color", "", "工作表标签颜色,Hex 格式如 #FF0000;传空字符串清除颜色")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetCopyCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("copy", "复制工作表", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if cmd.Flags().Changed("name") || cmd.Flags().Changed("title") {
|
||
params["title"] = sheetNameFlag(cmd)
|
||
}
|
||
if cmd.Flags().Changed("index") {
|
||
index, _ := cmd.Flags().GetInt("index")
|
||
if index < 0 {
|
||
return apperrors.NewValidation("--index must be >= 0")
|
||
}
|
||
params["index"] = index
|
||
}
|
||
return runSheetTool(cmd, runner, "copy_sheet", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("name", "", "副本名称")
|
||
cmd.Flags().String("title", "", "--name alias")
|
||
_ = cmd.Flags().MarkHidden("title")
|
||
cmd.Flags().Int("index", 0, "副本位置索引,0-based")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetDeleteSheetCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("delete-sheet", "删除工作表", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "delete_sheet", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newSheetRangeReadCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("read", "读取工作表数据", func(cmd *cobra.Command, _ []string) error {
|
||
node, err := sheetRequiredFlag(cmd, "node")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{"nodeId": node}
|
||
sheetAddStringParam(cmd, params, "sheetId", "sheet-id")
|
||
sheetAddStringParam(cmd, params, "range", "range")
|
||
sheetAddStringParam(cmd, params, "valueRenderOption", "value-render-option")
|
||
result, err := sheetInvocationResult(cmd, runner, "sheet", "get_cell_infos", params)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// 剥掉 MCP 框架填充的全 null dataValidation / hyperlink 空壳,
|
||
// 使"清除后回读不再含该 key"成立(与 wukong 对齐)。dry-run 不投影。
|
||
if !commandDryRun(cmd) {
|
||
sheetCleanCellInfos(result.Response)
|
||
// Project a flat `values` 2D array alongside the rich `cells` so
|
||
// consumers expecting wukong's get_range shape (values: [["a"]])
|
||
// work while `cells` (styles/richText) stays available.
|
||
sheetProjectFlatValues(result.Response)
|
||
}
|
||
return writeCommandPayload(cmd, result)
|
||
})
|
||
cmd.Aliases = []string{"get"}
|
||
addSheetNodeFlags(cmd)
|
||
cmd.Flags().String("sheet-id", "", "工作表 ID 或名称")
|
||
cmd.Flags().String("range", "", "读取范围,A1 表示法")
|
||
cmd.Flags().String("value-render-option", "", "formatted_value | raw_value | formula")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetRangeUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("update", "更新工作表指定区域内容", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "range"); err != nil {
|
||
return err
|
||
}
|
||
hasValues := sheetStringFlag(cmd, "values") != ""
|
||
hasHyperlinks := sheetStringFlag(cmd, "hyperlinks") != ""
|
||
if !hasValues && !hasHyperlinks {
|
||
return apperrors.NewValidation("--values 或 --hyperlinks 至少传入一项")
|
||
}
|
||
var cells [][]any
|
||
if hasValues {
|
||
if err := sheetParseJSONFlag(cmd, "values", &cells); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
// --hyperlinks overlays cell-level links onto the same grid (matches
|
||
// wukong update_range: hyperlinks coexist with values, hyperlinks win).
|
||
// Each item is wukong's {type:path,link,text}, which is exactly open's
|
||
// cell `hyperlink` field shape.
|
||
if hasHyperlinks {
|
||
var hyperlinks [][]any
|
||
if err := sheetParseJSONFlag(cmd, "hyperlinks", &hyperlinks); err != nil {
|
||
return err
|
||
}
|
||
merged, err := sheetMergeHyperlinks(cells, hyperlinks)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cells = merged
|
||
}
|
||
for i, row := range cells {
|
||
for j, cell := range row {
|
||
path := fmt.Sprintf("--values[%d][%d]", i, j)
|
||
if cell == nil {
|
||
// null clears the cell content (matches wukong's update_range
|
||
// pass-through). Use {} (keep-original) for "leave unchanged".
|
||
cells[i][j] = map[string]any{"type": "text", "text": ""}
|
||
continue
|
||
}
|
||
// Auto-wrap a scalar cell (string / number / bool) into a text
|
||
// cell so the simple form --values '[["姓名","部门"]]' works, the
|
||
// same shape `append` and wukong's update_range accept. Explicit
|
||
// object cells fall through to the rich validation below.
|
||
if wrapped, ok := sheetWrapScalarCell(cell); ok {
|
||
cells[i][j] = wrapped
|
||
continue
|
||
}
|
||
cellMap, ok := cell.(map[string]any)
|
||
if !ok {
|
||
return apperrors.NewValidation(fmt.Sprintf("%s: 必须为 object(如 {\"type\":\"text\",\"text\":\"...\"})、纯字符串/数字,或 {} 表示保留原值", path))
|
||
}
|
||
// {} 空对象 = 跳过该单元格,保留原值不变
|
||
if len(cellMap) == 0 {
|
||
continue
|
||
}
|
||
if err := sheetValidateComplexValueCell(cellMap, path); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return runSheetTool(cmd, runner, "set_cell_range", map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"rangeAddress": sheetStringFlag(cmd, "range"),
|
||
"cells": cells,
|
||
})
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("range", "", "目标单元格区域地址 (必填)")
|
||
cmd.Flags().String("values", "", "单元格内容二维 JSON 数组 (--values 或 --hyperlinks 至少一项)")
|
||
cmd.Flags().String("hyperlinks", "", "超链接二维 JSON 数组,每项 {type:path,link,text};与 --values 共存时按位叠加")
|
||
return cmd
|
||
}
|
||
|
||
// sheetMergeHyperlinks overlays a hyperlinks grid onto a values cell grid so
|
||
// `sheet range update` accepts the separate --hyperlinks flag (wukong shape).
|
||
// Each non-null hyperlink item is wukong's {type:path,link,text}, attached as
|
||
// the cell's `hyperlink` field; a scalar value cell is first wrapped to a text
|
||
// cell so the link has a host cell. Returns a grid sized to the larger of the
|
||
// two inputs.
|
||
func sheetMergeHyperlinks(cells [][]any, hyperlinks [][]any) ([][]any, error) {
|
||
rows := len(cells)
|
||
if len(hyperlinks) > rows {
|
||
rows = len(hyperlinks)
|
||
}
|
||
out := make([][]any, rows)
|
||
for i := 0; i < rows; i++ {
|
||
var valRow, linkRow []any
|
||
if i < len(cells) {
|
||
valRow = cells[i]
|
||
}
|
||
if i < len(hyperlinks) {
|
||
linkRow = hyperlinks[i]
|
||
}
|
||
cols := len(valRow)
|
||
if len(linkRow) > cols {
|
||
cols = len(linkRow)
|
||
}
|
||
row := make([]any, cols)
|
||
for j := 0; j < cols; j++ {
|
||
if j < len(valRow) {
|
||
row[j] = valRow[j]
|
||
}
|
||
if j < len(linkRow) && linkRow[j] != nil {
|
||
cell, ok := row[j].(map[string]any)
|
||
if !ok {
|
||
// No (or scalar) value cell here — host the link on a cell.
|
||
if wrapped, wok := sheetWrapScalarCell(row[j]); wok {
|
||
cell = wrapped
|
||
} else {
|
||
cell = map[string]any{}
|
||
}
|
||
}
|
||
cell["hyperlink"] = linkRow[j]
|
||
row[j] = cell
|
||
}
|
||
}
|
||
out[i] = row
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func newSheetRangeClearCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("clear", "清除工作表指定区域", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "range"); err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"range": sheetStringFlag(cmd, "range"),
|
||
}
|
||
sheetAddStringParam(cmd, params, "type", "type")
|
||
return runSheetTool(cmd, runner, "clear_range", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("range", "", "清除范围,A1 表示法 (必填)")
|
||
cmd.Flags().String("type", "", "content | format | all")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetRangeSortCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("sort", "对工作表指定区域排序", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "range", "sort-keys"); err != nil {
|
||
return err
|
||
}
|
||
var sortKeys []any
|
||
if err := sheetParseJSONFlag(cmd, "sort-keys", &sortKeys); err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"range": sheetStringFlag(cmd, "range"),
|
||
"sortKeys": sortKeys,
|
||
}
|
||
if v, _ := cmd.Flags().GetBool("has-header"); v {
|
||
params["hasHeader"] = true
|
||
}
|
||
return runSheetTool(cmd, runner, "sort_range", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("range", "", "排序范围,A1 表示法 (必填)")
|
||
cmd.Flags().String("sort-keys", "", "排序规则 JSON 数组 (必填)")
|
||
cmd.Flags().Bool("has-header", false, "首行是否为表头")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetRangeFillCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("fill", "自动填充工作表指定区域", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "source-range", "target-range"); err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"sourceRange": sheetStringFlag(cmd, "source-range"),
|
||
"destinationRange": sheetStringFlag(cmd, "target-range"),
|
||
}
|
||
sheetAddStringParam(cmd, params, "fillType", "fill-type")
|
||
return runSheetTool(cmd, runner, "fill_range", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("source-range", "", "源数据范围,A1 表示法 (必填)")
|
||
cmd.Flags().String("target-range", "", "目标填充范围,A1 表示法 (必填)")
|
||
cmd.Flags().String("fill-type", "", "copy | onlystyle | withoutstyle")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetRangeCopyCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("copy-to", "复制工作表指定区域到目标位置", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetRangeTransferParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
sheetAddStringParam(cmd, params, "pasteType", "paste-type")
|
||
return runSheetTool(cmd, runner, "copy_range", params)
|
||
})
|
||
addSheetRangeTransferFlags(cmd)
|
||
cmd.Flags().String("paste-type", "", "values | formulas | formats | all")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetRangeMoveCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("move-to", "移动工作表指定区域到目标位置", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetRangeTransferParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "move_range", params)
|
||
})
|
||
addSheetRangeTransferFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFindCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("find", "在工作表中搜索单元格内容", func(cmd *cobra.Command, _ []string) error {
|
||
query, err := sheetRequiredFlagOrFallback(cmd, "query", "find", "keyword")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["text"] = query
|
||
sheetAddStringParam(cmd, params, "range", "range")
|
||
for flag, key := range map[string]string{
|
||
"match-case": "matchCase",
|
||
"match-entire-cell": "matchEntireCell",
|
||
"use-regexp": "useRegExp",
|
||
"match-formula": "matchFormulaText",
|
||
"include-hidden": "includeHidden",
|
||
} {
|
||
v, _ := cmd.Flags().GetBool(flag)
|
||
params[key] = v
|
||
}
|
||
return runSheetTool(cmd, runner, "find_cells", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("query", "", "搜索文本 (必填)")
|
||
cmd.Flags().String("find", "", "--query alias")
|
||
_ = cmd.Flags().MarkHidden("find")
|
||
addSheetHiddenStringFlag(cmd, "keyword", "--query 的跨产品别名")
|
||
cmd.Flags().String("range", "", "搜索范围,A1 表示法")
|
||
cmd.Flags().Bool("match-case", true, "区分大小写")
|
||
cmd.Flags().Bool("match-entire-cell", false, "完整单元格匹配")
|
||
cmd.Flags().Bool("use-regexp", false, "启用正则表达式搜索")
|
||
cmd.Flags().Bool("match-formula", false, "搜索公式文本")
|
||
cmd.Flags().Bool("include-hidden", false, "包含隐藏单元格")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetAppendCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("append", "在工作表末尾追加数据", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "values"); err != nil {
|
||
return err
|
||
}
|
||
var values [][]any
|
||
if err := sheetParseJSONFlag(cmd, "values", &values); err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "append_rows", map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"values": values,
|
||
})
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("values", "", "追加数据二维 JSON 数组 (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetCSVPutCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("csv-put", "将 CSV 数据写入表格指定位置", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "csv", "start-cell"); err != nil {
|
||
return err
|
||
}
|
||
csvContent := sheetStringFlag(cmd, "csv")
|
||
switch {
|
||
case csvContent == "-":
|
||
data, err := io.ReadAll(os.Stdin)
|
||
if err != nil {
|
||
return fmt.Errorf("read stdin failed: %w", err)
|
||
}
|
||
csvContent = string(data)
|
||
case strings.HasPrefix(csvContent, "@"):
|
||
data, err := os.ReadFile(strings.TrimPrefix(csvContent, "@"))
|
||
if err != nil {
|
||
return fmt.Errorf("read CSV file failed: %w", err)
|
||
}
|
||
csvContent = string(data)
|
||
}
|
||
csvContent = strings.ReplaceAll(csvContent, "\r", "")
|
||
csvContent = strings.TrimPrefix(csvContent, "\xef\xbb\xbf")
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"csv": csvContent,
|
||
"startCell": sheetStringFlag(cmd, "start-cell"),
|
||
}
|
||
if cmd.Flags().Changed("allow-overwrite") {
|
||
v, _ := cmd.Flags().GetBool("allow-overwrite")
|
||
params["allowOverwrite"] = v
|
||
}
|
||
return runSheetTool(cmd, runner, "set_range_from_csv", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("csv", "", "CSV 文本、@文件路径 或 - (必填)")
|
||
cmd.Flags().String("start-cell", "", "起始单元格,A1 表示法 (必填)")
|
||
cmd.Flags().Bool("allow-overwrite", false, "允许覆盖已有数据")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetCSVGetCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("csv-get", "以 CSV 格式读取工作表数据", func(cmd *cobra.Command, _ []string) error {
|
||
node, err := sheetRequiredFlag(cmd, "node")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{"nodeId": node}
|
||
sheetAddStringParam(cmd, params, "sheetId", "sheet-id")
|
||
sheetAddStringParam(cmd, params, "range", "range")
|
||
sheetAddStringParam(cmd, params, "valueRenderOption", "value-render-option")
|
||
if cmd.Flags().Changed("max-chars") {
|
||
v, _ := cmd.Flags().GetInt("max-chars")
|
||
params["maxChars"] = v
|
||
}
|
||
return runSheetTool(cmd, runner, "get_range_as_csv", params)
|
||
})
|
||
addSheetNodeFlags(cmd)
|
||
cmd.Flags().String("sheet-id", "", "工作表 ID 或名称")
|
||
cmd.Flags().String("range", "", "读取范围,A1 表示法")
|
||
cmd.Flags().String("value-render-option", "", "formatted_value | raw_value | formula")
|
||
cmd.Flags().Int("max-chars", 0, "CSV 最大字符数")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetInsertDimensionCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetDimensionPositionCommand(runner, "insert-dimension", "在指定位置插入行或列", "insert_dimension")
|
||
}
|
||
|
||
func newSheetDeleteDimensionCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetDimensionPositionCommand(runner, "delete-dimension", "删除指定位置的行或列", "delete_dimension")
|
||
}
|
||
|
||
func newSheetDimensionPositionCommand(runner executor.Runner, use, short, tool string) *cobra.Command {
|
||
cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "dimension", "position", "length"); err != nil {
|
||
return err
|
||
}
|
||
dimension, err := sheetDimension(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
length, err := sheetPositiveIntStringFlag(cmd, "length", 5000)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, tool, map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"dimension": dimension,
|
||
"position": sheetStringFlag(cmd, "position"),
|
||
"length": length,
|
||
})
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)")
|
||
cmd.Flags().String("position", "", "位置,A1 表示法 (必填)")
|
||
cmd.Flags().String("length", "", "数量,正整数 (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetUpdateDimensionCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("update-dimension", "更新指定范围行/列属性", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "dimension", "start-index", "length"); err != nil {
|
||
return err
|
||
}
|
||
dimension, err := sheetDimension(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
length, err := sheetPositiveIntStringFlag(cmd, "length", 5000)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
hiddenChanged := cmd.Flags().Changed("hidden")
|
||
pixelChanged := cmd.Flags().Changed("pixel-size")
|
||
if !hiddenChanged && !pixelChanged {
|
||
return apperrors.NewValidation("at least one of --hidden or --pixel-size is required")
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"dimension": dimension,
|
||
"startIndex": sheetStringFlag(cmd, "start-index"),
|
||
"length": length,
|
||
}
|
||
if hiddenChanged {
|
||
v, _ := cmd.Flags().GetBool("hidden")
|
||
params["hidden"] = v
|
||
}
|
||
if pixelChanged {
|
||
v, _ := cmd.Flags().GetInt("pixel-size")
|
||
if v < 0 {
|
||
return apperrors.NewValidation("--pixel-size must be >= 0")
|
||
}
|
||
params["pixelSize"] = v
|
||
}
|
||
return runSheetTool(cmd, runner, "update_dimension", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)")
|
||
cmd.Flags().String("start-index", "", "起始位置,A1 表示法 (必填)")
|
||
cmd.Flags().String("length", "", "数量,正整数 (必填)")
|
||
cmd.Flags().Bool("hidden", false, "是否隐藏")
|
||
cmd.Flags().Int("pixel-size", 0, "行高或列宽")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetMoveDimensionCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("move-dimension", "移动行或列到指定位置", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "dimension", "start-index", "end-index", "destination-index"); err != nil {
|
||
return err
|
||
}
|
||
dimension, err := sheetDimension(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "move_dimension", map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"dimension": dimension,
|
||
"startIndex": sheetStringFlag(cmd, "start-index"),
|
||
"endIndex": sheetStringFlag(cmd, "end-index"),
|
||
"destinationIndex": sheetStringFlag(cmd, "destination-index"),
|
||
})
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)")
|
||
cmd.Flags().String("start-index", "", "源起始位置 (必填)")
|
||
cmd.Flags().String("end-index", "", "源结束位置 (必填)")
|
||
cmd.Flags().String("destination-index", "", "目标位置 (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetAddDimensionCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("add-dimension", "在末尾追加空行或空列", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "dimension"); err != nil {
|
||
return err
|
||
}
|
||
dimension, err := sheetDimension(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
length, _ := cmd.Flags().GetInt("length")
|
||
if length < 1 || length > 5000 {
|
||
return apperrors.NewValidation("--length must be between 1 and 5000")
|
||
}
|
||
return runSheetTool(cmd, runner, "add_dimension", map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"dimension": dimension,
|
||
"length": length,
|
||
})
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)")
|
||
cmd.Flags().Int("length", 0, "追加数量,正整数 (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetMergeCellsCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("merge-cells", "合并单元格", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetRangeAddressParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
sheetAddStringParam(cmd, params, "mergeType", "merge-type")
|
||
return runSheetTool(cmd, runner, "merge_cells", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("range", "", "目标单元格区域地址 (必填)")
|
||
cmd.Flags().String("merge-type", "", "mergeAll | mergeRows | mergeColumns")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetUnmergeCellsCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("unmerge-cells", "取消合并单元格", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetRangeAddressParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "unmerge_range", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("range", "", "取消合并的范围 (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetSetDropdownCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("set-dropdown", "设置下拉列表", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "range", "options"); err != nil {
|
||
return err
|
||
}
|
||
var options []map[string]any
|
||
if err := sheetParseJSONFlag(cmd, "options", &options); err != nil {
|
||
return err
|
||
}
|
||
if len(options) == 0 {
|
||
return apperrors.NewValidation("--options must contain at least one item")
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"range": sheetStringFlag(cmd, "range"),
|
||
"options": options,
|
||
}
|
||
if v, _ := cmd.Flags().GetBool("multi-select"); v {
|
||
params["enableMultiSelect"] = true
|
||
}
|
||
return runSheetTool(cmd, runner, "set_dropdown_lists", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("range", "", "目标单元格范围 (必填)")
|
||
cmd.Flags().String("options", "", "下拉选项 JSON 数组 (必填)")
|
||
cmd.Flags().Bool("multi-select", false, "是否允许多选")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetGetDropdownCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetRangeToolCommand(runner, "get-dropdown", "获取下拉列表配置", "get_dropdown_lists", "range")
|
||
}
|
||
|
||
func newSheetDeleteDropdownCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetRangeToolCommand(runner, "delete-dropdown", "删除下拉列表", "delete_dropdown_lists", "range")
|
||
}
|
||
|
||
func newSheetReplaceCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("replace", "查找替换文本", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "find"); err != nil {
|
||
return err
|
||
}
|
||
if !cmd.Flags().Changed("replacement") {
|
||
return apperrors.NewValidation("--replacement is required")
|
||
}
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["text"] = sheetStringFlag(cmd, "find")
|
||
params["replaceText"] = sheetStringFlag(cmd, "replacement")
|
||
sheetAddStringParam(cmd, params, "range", "range")
|
||
for flag, key := range map[string]string{
|
||
"match-case": "matchCase",
|
||
"match-entire-cell": "matchEntireCell",
|
||
"use-regexp": "useRegExp",
|
||
"include-hidden": "includeHidden",
|
||
} {
|
||
v, _ := cmd.Flags().GetBool(flag)
|
||
params[key] = v
|
||
}
|
||
return runSheetTool(cmd, runner, "replace_all", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("find", "", "查找文本 (必填)")
|
||
cmd.Flags().String("replacement", "", "替换文本 (必填,可为空)")
|
||
cmd.Flags().String("range", "", "替换范围,A1 表示法")
|
||
cmd.Flags().Bool("match-case", false, "区分大小写")
|
||
cmd.Flags().Bool("match-entire-cell", false, "完整单元格匹配")
|
||
cmd.Flags().Bool("use-regexp", false, "启用正则表达式匹配")
|
||
cmd.Flags().Bool("include-hidden", false, "包含隐藏行/列")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterGetCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetBaseToolCommand(runner, "get", "获取全局筛选信息", "get_filter")
|
||
}
|
||
|
||
func newSheetFilterDeleteCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetBaseToolCommand(runner, "delete", "删除全局筛选", "delete_filter")
|
||
}
|
||
|
||
func newSheetFilterCreateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("create", "创建全局筛选", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "range"); err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"range": sheetStringFlag(cmd, "range"),
|
||
}
|
||
if sheetStringFlag(cmd, "criteria") != "" {
|
||
var criteria []any
|
||
if err := sheetParseJSONFlag(cmd, "criteria", &criteria); err != nil {
|
||
return err
|
||
}
|
||
params["criteria"] = criteria
|
||
}
|
||
return runSheetTool(cmd, runner, "create_filter", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("range", "", "筛选范围,A1 表示法 (必填)")
|
||
cmd.Flags().String("criteria", "", "筛选条件 JSON 数组")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("update", "批量更新筛选条件", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "criteria"); err != nil {
|
||
return err
|
||
}
|
||
var criteria []any
|
||
if err := sheetParseJSONFlag(cmd, "criteria", &criteria); err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "update_filter", map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"criteria": criteria,
|
||
})
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("criteria", "", "筛选条件 JSON 数组 (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterClearCriteriaCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetColumnCommand(runner, "clear-criteria", "清除单列筛选条件", "clear_filter_criteria")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterSortCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("sort", "筛选排序", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
column, _ := cmd.Flags().GetInt("column")
|
||
ascending, _ := cmd.Flags().GetBool("ascending")
|
||
params["field"] = map[string]any{"column": column, "ascending": ascending}
|
||
return runSheetTool(cmd, runner, "sort_filter", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().Int("column", 0, "排序列偏移量,从 0 开始")
|
||
cmd.Flags().Bool("ascending", true, "是否升序")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterViewListCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetBaseToolCommand(runner, "list", "获取所有筛选视图", "get_filter_views")
|
||
}
|
||
|
||
func newSheetFilterViewCreateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("create", "创建筛选视图", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "name", "range"); err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"name": sheetStringFlag(cmd, "name"),
|
||
"range": sheetStringFlag(cmd, "range"),
|
||
}
|
||
if sheetStringFlag(cmd, "criteria") != "" {
|
||
var criteria []any
|
||
if err := sheetParseJSONFlag(cmd, "criteria", &criteria); err != nil {
|
||
return err
|
||
}
|
||
params["criteria"] = criteria
|
||
}
|
||
return runSheetTool(cmd, runner, "create_filter_view", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("name", "", "筛选视图名称 (必填)")
|
||
cmd.Flags().String("range", "", "筛选视图范围,A1 表示法 (必填)")
|
||
cmd.Flags().String("criteria", "", "筛选条件 JSON 数组")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterViewUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("update", "更新筛选视图属性", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "filter-view-id"); err != nil {
|
||
return err
|
||
}
|
||
params := sheetFilterViewBaseParams(cmd)
|
||
changed := false
|
||
if cmd.Flags().Changed("name") {
|
||
params["name"] = sheetStringFlag(cmd, "name")
|
||
changed = true
|
||
}
|
||
if cmd.Flags().Changed("range") {
|
||
params["range"] = sheetStringFlag(cmd, "range")
|
||
changed = true
|
||
}
|
||
if cmd.Flags().Changed("criteria") {
|
||
var criteria []any
|
||
if err := sheetParseJSONFlag(cmd, "criteria", &criteria); err != nil {
|
||
return err
|
||
}
|
||
params["criteria"] = criteria
|
||
changed = true
|
||
}
|
||
if !changed {
|
||
return apperrors.NewValidation("at least one of --name, --range or --criteria is required")
|
||
}
|
||
return runSheetTool(cmd, runner, "update_filter_view", params)
|
||
})
|
||
addSheetFilterViewBaseFlags(cmd)
|
||
cmd.Flags().String("name", "", "筛选视图新名称")
|
||
cmd.Flags().String("range", "", "筛选视图新范围")
|
||
cmd.Flags().String("criteria", "", "筛选条件 JSON 数组")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterViewDeleteCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("delete", "删除筛选视图", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "filter-view-id"); err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "delete_filter_view", sheetFilterViewBaseParams(cmd))
|
||
})
|
||
addSheetFilterViewBaseFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterViewUpdateCriteriaCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("update-criteria", "更新筛选视图列条件", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "filter-view-id", "filter-criteria"); err != nil {
|
||
return err
|
||
}
|
||
params := sheetFilterViewBaseParams(cmd)
|
||
column, _ := cmd.Flags().GetInt("column")
|
||
if column < 0 {
|
||
return apperrors.NewValidation("--column must be >= 0")
|
||
}
|
||
var filterCriteria map[string]any
|
||
if err := sheetParseJSONFlag(cmd, "filter-criteria", &filterCriteria); err != nil {
|
||
return err
|
||
}
|
||
params["column"] = column
|
||
params["filterCriteria"] = filterCriteria
|
||
return runSheetTool(cmd, runner, "set_filter_view_criteria", params)
|
||
})
|
||
addSheetFilterViewBaseFlags(cmd)
|
||
cmd.Flags().Int("column", 0, "列偏移量,从 0 开始")
|
||
cmd.Flags().String("filter-criteria", "", "筛选条件 JSON 对象 (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterViewDeleteCriteriaCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetColumnCommand(runner, "delete-criteria", "删除筛选视图列条件", "clear_filter_view_criteria")
|
||
cmd.Flags().String("filter-view-id", "", "筛选视图 ID (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterViewInfoCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetFilterViewReadCommand(runner, "info", "获取单个筛选视图详情", "info")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterViewListCriteriaCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetFilterViewReadCommand(runner, "list-criteria", "列出筛选视图所有列条件", "list-criteria")
|
||
}
|
||
|
||
func newSheetFilterViewGetCriteriaCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetFilterViewReadCommand(runner, "get-criteria", "获取单列筛选条件", "get-criteria")
|
||
cmd.Flags().Int("column", 0, "列偏移量,从 0 开始")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFilterViewReadCommand(runner executor.Runner, use, short, mode string) *cobra.Command {
|
||
cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "filter-view-id"); err != nil {
|
||
return err
|
||
}
|
||
result, err := sheetInvocationResult(cmd, runner, "sheet", "get_filter_views", map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
filterViews := sheetResultFilterViews(result.Response)
|
||
// 不再对空列表提前返回成功:请求了具体 filter-view-id 却找不到(含一个都没有)
|
||
// 应报"未找到"错误,而非静默成功(与 wukong 对齐)。
|
||
view, err := sheetFindFilterView(filterViews, sheetStringFlag(cmd, "filter-view-id"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
switch mode {
|
||
case "info":
|
||
return writeCommandPayload(cmd, view)
|
||
case "list-criteria":
|
||
if criteria, ok := view["criteria"]; ok {
|
||
return writeCommandPayload(cmd, criteria)
|
||
}
|
||
return writeCommandPayload(cmd, map[string]any{})
|
||
case "get-criteria":
|
||
column, _ := cmd.Flags().GetInt("column")
|
||
if column < 0 {
|
||
return apperrors.NewValidation("--column must be >= 0")
|
||
}
|
||
criteria, _ := view["criteria"].(map[string]any)
|
||
if criteria == nil {
|
||
return apperrors.NewValidation("filter view has no criteria")
|
||
}
|
||
item, ok := criteria[strconv.Itoa(column)]
|
||
if !ok {
|
||
return apperrors.NewValidation("filter view column criteria not found")
|
||
}
|
||
return writeCommandPayload(cmd, item)
|
||
default:
|
||
return writeCommandPayload(cmd, result)
|
||
}
|
||
})
|
||
addSheetFilterViewBaseFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newSheetCondFormatListCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("list", "获取条件格式规则", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
sheetAddStringParam(cmd, params, "ruleId", "rule-id")
|
||
return runSheetTool(cmd, runner, "get_cond_format", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("rule-id", "", "条件格式规则 ID")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetCondFormatCreateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("create", "创建条件格式规则", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetCondFormatMutationBase(cmd, false)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "create_cond_format", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("ranges", "", "应用范围 JSON 数组 (必填)")
|
||
cmd.Flags().String("condition", "", "条件类型及参数 JSON 对象 (必填)")
|
||
cmd.Flags().String("cell-style", "", "单元格样式 JSON 对象")
|
||
cmd.Flags().String("data-bar-style", "", "数据条样式 JSON 对象")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetCondFormatUpdateCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("update", "更新条件格式规则", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetCondFormatMutationBase(cmd, true)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(params) <= 3 {
|
||
return apperrors.NewValidation("at least one of --ranges, --condition, --cell-style or --data-bar-style is required")
|
||
}
|
||
return runSheetTool(cmd, runner, "update_cond_format", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("rule-id", "", "条件格式规则 ID (必填)")
|
||
cmd.Flags().String("ranges", "", "应用范围 JSON 数组")
|
||
cmd.Flags().String("condition", "", "条件类型及参数 JSON 对象")
|
||
cmd.Flags().String("cell-style", "", "单元格样式 JSON 对象")
|
||
cmd.Flags().String("data-bar-style", "", "数据条样式 JSON 对象")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetCondFormatDeleteCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("delete", "删除条件格式规则", func(cmd *cobra.Command, _ []string) error {
|
||
if ok, _ := cmd.Flags().GetBool("yes"); !ok && !commandDryRun(cmd) {
|
||
return apperrors.NewValidation("delete cond-format requires --yes")
|
||
}
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ruleID, err := sheetRequiredFlag(cmd, "rule-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["ruleId"] = ruleID
|
||
return runSheetTool(cmd, runner, "delete_cond_format", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("rule-id", "", "条件格式规则 ID (必填)")
|
||
cmd.Flags().Bool("yes", false, "确认删除")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetCreateFloatImageCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("create-float-image", "创建浮动图片", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "src", "range"); err != nil {
|
||
return err
|
||
}
|
||
width, height, err := sheetPositiveSize(cmd, true)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"src": sheetStringFlag(cmd, "src"),
|
||
"range": sheetStringFlag(cmd, "range"),
|
||
"width": width,
|
||
"height": height,
|
||
}
|
||
sheetAddChangedIntParam(cmd, params, "offsetX", "offset-x")
|
||
sheetAddChangedIntParam(cmd, params, "offsetY", "offset-y")
|
||
return runSheetTool(cmd, runner, "create_float_image", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("src", "", "图片资源路径 (必填)")
|
||
cmd.Flags().String("range", "", "锚点单元格 (必填)")
|
||
cmd.Flags().Int("width", 0, "图片宽度,像素 (必填)")
|
||
cmd.Flags().Int("height", 0, "图片高度,像素 (必填)")
|
||
cmd.Flags().Int("offset-x", 0, "水平偏移量")
|
||
cmd.Flags().Int("offset-y", 0, "垂直偏移量")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetGetFloatImageCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetFloatImageIDCommand(runner, "get-float-image", "获取浮动图片详情", "get_float_image")
|
||
}
|
||
|
||
func newSheetListFloatImagesCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetBaseToolCommand(runner, "list-float-images", "列出工作表所有浮动图片", "list_float_images")
|
||
}
|
||
|
||
func newSheetUpdateFloatImageCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("update-float-image", "更新浮动图片属性", func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetFloatImageBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
changed := false
|
||
for flag, key := range map[string]string{"src": "src", "range": "range"} {
|
||
if cmd.Flags().Changed(flag) {
|
||
params[key] = sheetStringFlag(cmd, flag)
|
||
changed = true
|
||
}
|
||
}
|
||
for flag, key := range map[string]string{"width": "width", "height": "height", "offset-x": "offsetX", "offset-y": "offsetY"} {
|
||
if cmd.Flags().Changed(flag) {
|
||
v, _ := cmd.Flags().GetInt(flag)
|
||
if (flag == "width" || flag == "height") && v <= 0 {
|
||
return apperrors.NewValidation("--" + flag + " must be > 0")
|
||
}
|
||
if (flag == "offset-x" || flag == "offset-y") && v < 0 {
|
||
return apperrors.NewValidation("--" + flag + " must be >= 0")
|
||
}
|
||
params[key] = v
|
||
changed = true
|
||
}
|
||
}
|
||
if !changed {
|
||
return apperrors.NewValidation("at least one float image field is required")
|
||
}
|
||
return runSheetTool(cmd, runner, "update_float_image", params)
|
||
})
|
||
addSheetFloatImageBaseFlags(cmd)
|
||
cmd.Flags().String("src", "", "新的图片资源路径")
|
||
cmd.Flags().String("range", "", "新的锚点单元格")
|
||
cmd.Flags().Int("width", 0, "新的图片宽度")
|
||
cmd.Flags().Int("height", 0, "新的图片高度")
|
||
cmd.Flags().Int("offset-x", 0, "新的水平偏移量")
|
||
cmd.Flags().Int("offset-y", 0, "新的垂直偏移量")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetDeleteFloatImageCommand(runner executor.Runner) *cobra.Command {
|
||
return newSheetFloatImageIDCommand(runner, "delete-float-image", "删除浮动图片", "delete_float_image")
|
||
}
|
||
|
||
func newSheetMediaUploadCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("media-upload", "上传附件到表格", func(cmd *cobra.Command, _ []string) error {
|
||
return runSheetAttachmentUpload(cmd, runner, false)
|
||
})
|
||
addSheetNodeFlags(cmd)
|
||
cmd.Flags().String("file", "", "本地文件路径 (必填)")
|
||
cmd.Flags().String("name", "", "附件显示名称")
|
||
cmd.Flags().String("mime-type", "", "文件 MIME 类型")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetWriteImageCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("write-image", "上传图片并写入表格单元格", func(cmd *cobra.Command, _ []string) error {
|
||
return runSheetAttachmentUpload(cmd, runner, true)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("range", "", "目标单元格区域地址 (必填)")
|
||
cmd.Flags().String("file", "", "本地图片文件路径 (必填)")
|
||
cmd.Flags().String("name", "", "图片显示名称")
|
||
cmd.Flags().String("mime-type", "", "文件 MIME 类型")
|
||
cmd.Flags().Int("width", 0, "图片显示宽度")
|
||
cmd.Flags().Int("height", 0, "图片显示高度")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetExportCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("export", "导出表格为 xlsx", func(cmd *cobra.Command, _ []string) error {
|
||
node, err := sheetRequiredFlag(cmd, "node")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
outputPath := sheetStringFlag(cmd, "output")
|
||
if commandDryRun(cmd) {
|
||
return runSheetTool(cmd, runner, "submit_export_job", map[string]any{"nodeId": node, "exportFormat": "xlsx"})
|
||
}
|
||
submit, err := sheetInvocationResult(cmd, runner, "sheet", "submit_export_job", map[string]any{"nodeId": node, "exportFormat": "xlsx"})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
jobID := sheetStringFromResponse(submit.Response, "jobId")
|
||
if jobID == "" {
|
||
return writeCommandPayload(cmd, submit)
|
||
}
|
||
downloadURL, status, err := pollSheetExport(cmd, runner, jobID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if outputPath != "" && downloadURL != "" {
|
||
if fi, statErr := os.Stat(outputPath); statErr == nil && fi.IsDir() {
|
||
outputPath = filepath.Join(outputPath, sheetExportFilename(downloadURL, jobID))
|
||
}
|
||
if err := sheetHTTPGet(cmd, downloadURL, outputPath); err != nil {
|
||
return err
|
||
}
|
||
out := map[string]any{"jobId": jobID, "status": status, "output": outputPath}
|
||
if downloadURL != "" {
|
||
out["downloadUrl"] = downloadURL
|
||
}
|
||
return writeCommandPayload(cmd, out)
|
||
}
|
||
// 无 --output:按 wukong 输出 KV 纯文本(jobId / downloadUrl),便于下游正则解析。
|
||
fmt.Fprintf(cmd.OutOrStdout(), "jobId: %s\n", jobID)
|
||
if downloadURL != "" {
|
||
fmt.Fprintf(cmd.OutOrStdout(), "downloadUrl: %s\n", downloadURL)
|
||
}
|
||
return nil
|
||
})
|
||
addSheetNodeFlags(cmd)
|
||
cmd.Flags().String("output", "", "本地保存路径")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetRangeToolCommand(runner executor.Runner, use, short, tool, rangeFlag string) *cobra.Command {
|
||
cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", rangeFlag); err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"range": sheetStringFlag(cmd, rangeFlag),
|
||
}
|
||
return runSheetTool(cmd, runner, tool, params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String(rangeFlag, "", "范围,A1 表示法 (必填)")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetBaseToolCommand(runner executor.Runner, use, short, tool string) *cobra.Command {
|
||
cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, tool, params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newSheetColumnCommand(runner executor.Runner, use, short, tool string) *cobra.Command {
|
||
cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if strings.Contains(use, "criteria") && strings.Contains(tool, "filter_view") {
|
||
filterViewID, err := sheetRequiredFlag(cmd, "filter-view-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params["filterViewId"] = filterViewID
|
||
}
|
||
column, _ := cmd.Flags().GetInt("column")
|
||
if column < 0 {
|
||
return apperrors.NewValidation("--column must be >= 0")
|
||
}
|
||
params["column"] = column
|
||
return runSheetTool(cmd, runner, tool, params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().Int("column", 0, "列偏移量,从 0 开始")
|
||
return cmd
|
||
}
|
||
|
||
func newSheetFloatImageIDCommand(runner executor.Runner, use, short, tool string) *cobra.Command {
|
||
cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error {
|
||
params, err := sheetFloatImageBaseParams(cmd)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, tool, params)
|
||
})
|
||
addSheetFloatImageBaseFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newSheetGroup(use, short string) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: use,
|
||
Short: short,
|
||
Args: cobra.NoArgs,
|
||
TraverseChildren: true,
|
||
DisableAutoGenTag: true,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
return cmd.Help()
|
||
},
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newSheetLeaf(use, short string, run func(*cobra.Command, []string) error) *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: use,
|
||
Short: short,
|
||
Args: cobra.NoArgs,
|
||
DisableAutoGenTag: true,
|
||
RunE: run,
|
||
}
|
||
preferLegacyLeaf(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func runSheetTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error {
|
||
result, err := sheetInvocationResult(cmd, runner, "sheet", tool, params)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return writeCommandPayload(cmd, result)
|
||
}
|
||
|
||
func sheetInvocationResult(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)
|
||
return runner.Run(cmd.Context(), invocation)
|
||
}
|
||
|
||
func addSheetNodeFlags(cmd *cobra.Command) {
|
||
cmd.Flags().String("node", "", "表格文档 ID 或 URL")
|
||
addSheetHiddenStringFlag(cmd, "url", "--node alias")
|
||
addSheetHiddenStringFlag(cmd, "id", "--node alias")
|
||
addSheetHiddenStringFlag(cmd, "node-id", "--node alias")
|
||
addSheetHiddenStringFlag(cmd, "doc-id", "--node alias")
|
||
addSheetHiddenStringFlag(cmd, "file-id", "--node alias")
|
||
}
|
||
|
||
func addSheetBaseFlags(cmd *cobra.Command) {
|
||
addSheetNodeFlags(cmd)
|
||
cmd.Flags().String("sheet-id", "", "工作表 ID 或名称")
|
||
}
|
||
|
||
func addSheetFilterViewBaseFlags(cmd *cobra.Command) {
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("filter-view-id", "", "筛选视图 ID")
|
||
}
|
||
|
||
func addSheetFloatImageBaseFlags(cmd *cobra.Command) {
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("float-image-id", "", "浮动图片 ID")
|
||
}
|
||
|
||
func addSheetRangeTransferFlags(cmd *cobra.Command) {
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("source-range", "", "源范围,A1 表示法 (必填)")
|
||
cmd.Flags().String("target-range", "", "目标位置,A1 表示法 (必填)")
|
||
cmd.Flags().String("target-sheet-id", "", "目标工作表 ID 或名称")
|
||
}
|
||
|
||
func addSheetHiddenStringFlag(cmd *cobra.Command, name, usage string) {
|
||
cmd.Flags().String(name, "", usage)
|
||
_ = cmd.Flags().MarkHidden(name)
|
||
}
|
||
|
||
func sheetBaseParams(cmd *cobra.Command) (map[string]any, error) {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id"); err != nil {
|
||
return nil, err
|
||
}
|
||
return map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
}, nil
|
||
}
|
||
|
||
func sheetFilterViewBaseParams(cmd *cobra.Command) map[string]any {
|
||
return map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"filterViewId": sheetStringFlag(cmd, "filter-view-id"),
|
||
}
|
||
}
|
||
|
||
func sheetFloatImageBaseParams(cmd *cobra.Command) (map[string]any, error) {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "float-image-id"); err != nil {
|
||
return nil, err
|
||
}
|
||
return map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"floatImageId": sheetStringFlag(cmd, "float-image-id"),
|
||
}, nil
|
||
}
|
||
|
||
func sheetRangeAddressParams(cmd *cobra.Command) (map[string]any, error) {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "range"); err != nil {
|
||
return nil, err
|
||
}
|
||
return map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"rangeAddress": sheetStringFlag(cmd, "range"),
|
||
}, nil
|
||
}
|
||
|
||
func sheetRangeTransferParams(cmd *cobra.Command) (map[string]any, error) {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "source-range", "target-range"); err != nil {
|
||
return nil, err
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"sourceRange": sheetStringFlag(cmd, "source-range"),
|
||
"destinationRange": sheetStringFlag(cmd, "target-range"),
|
||
}
|
||
sheetAddStringParam(cmd, params, "targetSheetId", "target-sheet-id")
|
||
return params, nil
|
||
}
|
||
|
||
func sheetNodeAndName(cmd *cobra.Command) (string, string, error) {
|
||
node, err := sheetRequiredFlag(cmd, "node")
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
name, err := sheetRequiredFlag(cmd, "name")
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
return node, name, nil
|
||
}
|
||
|
||
func sheetNameFlag(cmd *cobra.Command) string {
|
||
if v := sheetStringFlag(cmd, "name"); v != "" {
|
||
return v
|
||
}
|
||
return sheetStringFlag(cmd, "title")
|
||
}
|
||
|
||
func sheetStringFlag(cmd *cobra.Command, name string) string {
|
||
if v, _ := cmd.Flags().GetString(name); v != "" {
|
||
return v
|
||
}
|
||
if name == "node" {
|
||
for _, alias := range []string{"url", "id", "node-id", "doc-id", "file-id"} {
|
||
if flag := cmd.Flags().Lookup(alias); flag == nil {
|
||
continue
|
||
}
|
||
if v, _ := cmd.Flags().GetString(alias); v != "" {
|
||
return v
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func sheetFlagOrFallback(cmd *cobra.Command, primary string, aliases ...string) string {
|
||
for _, name := range append([]string{primary}, aliases...) {
|
||
if flag := cmd.Flags().Lookup(name); flag == nil {
|
||
continue
|
||
}
|
||
if v := sheetStringFlag(cmd, name); v != "" {
|
||
return v
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func sheetRequiredFlag(cmd *cobra.Command, name string) (string, error) {
|
||
if v := sheetStringFlag(cmd, name); v != "" {
|
||
return v, nil
|
||
}
|
||
return "", apperrors.NewValidation("--" + name + " is required")
|
||
}
|
||
|
||
func sheetRequiredFlagOrFallback(cmd *cobra.Command, primary string, aliases ...string) (string, error) {
|
||
if v := sheetFlagOrFallback(cmd, primary, aliases...); v != "" {
|
||
return v, nil
|
||
}
|
||
return "", apperrors.NewValidation("--" + primary + " is required")
|
||
}
|
||
|
||
func sheetValidateRequired(cmd *cobra.Command, names ...string) error {
|
||
for _, name := range names {
|
||
if sheetStringFlag(cmd, name) == "" {
|
||
return apperrors.NewValidation("--" + name + " is required")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func sheetAddStringParam(cmd *cobra.Command, params map[string]any, key string, flags ...string) {
|
||
if v := sheetFlagOrFallback(cmd, flags[0], flags[1:]...); v != "" {
|
||
params[key] = v
|
||
}
|
||
}
|
||
|
||
func sheetAddChangedIntParam(cmd *cobra.Command, params map[string]any, key, flag string) {
|
||
if cmd.Flags().Changed(flag) {
|
||
v, _ := cmd.Flags().GetInt(flag)
|
||
params[key] = v
|
||
}
|
||
}
|
||
|
||
// sheetWrapScalarCell converts a bare scalar cell value (string / number /
|
||
// bool) into a {type:text,text:"..."} cell object accepted by set_cell_range.
|
||
// Returns ok=false for non-scalar values (maps, slices) so the caller can apply
|
||
// its richer object validation. This lets `sheet range update` accept the same
|
||
// plain `[["a","b"]]` shape as `sheet append` and the wukong update_range tool.
|
||
func sheetWrapScalarCell(cell any) (map[string]any, bool) {
|
||
switch v := cell.(type) {
|
||
case string:
|
||
return map[string]any{"type": "text", "text": v}, true
|
||
case float64:
|
||
return map[string]any{"type": "text", "text": strconv.FormatFloat(v, 'f', -1, 64)}, true
|
||
case json.Number:
|
||
return map[string]any{"type": "text", "text": v.String()}, true
|
||
case bool:
|
||
return map[string]any{"type": "text", "text": strconv.FormatBool(v)}, true
|
||
}
|
||
return nil, false
|
||
}
|
||
|
||
func sheetParseJSONFlag(cmd *cobra.Command, flag string, out any) error {
|
||
// Resolve @file / @- so large 2D ranges, criteria and sort-keys can come
|
||
// from a file or stdin instead of inline shell-quoted JSON. ResolveInputSource
|
||
// passes values not starting with "@" through unchanged.
|
||
raw, err := cli.ResolveInputSource(sheetStringFlag(cmd, flag), flag, cli.NewStdinGuard())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := json.Unmarshal([]byte(raw), out); err != nil {
|
||
return fmt.Errorf("--%s JSON parse failed: %w", flag, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func sheetDimension(cmd *cobra.Command) (string, error) {
|
||
dimension := sheetStringFlag(cmd, "dimension")
|
||
switch dimension {
|
||
case "ROW":
|
||
dimension = "ROWS"
|
||
case "COLUMN":
|
||
dimension = "COLUMNS"
|
||
}
|
||
if dimension != "ROWS" && dimension != "COLUMNS" {
|
||
return "", apperrors.NewValidation("--dimension must be ROWS or COLUMNS")
|
||
}
|
||
return dimension, nil
|
||
}
|
||
|
||
func sheetPositiveIntStringFlag(cmd *cobra.Command, flag string, max int) (int, error) {
|
||
raw := sheetStringFlag(cmd, flag)
|
||
n, err := strconv.Atoi(raw)
|
||
if err != nil || n < 1 {
|
||
return 0, apperrors.NewValidation("--" + flag + " must be a positive integer")
|
||
}
|
||
if max > 0 && n > max {
|
||
return 0, apperrors.NewValidation(fmt.Sprintf("--%s must be <= %d", flag, max))
|
||
}
|
||
return n, nil
|
||
}
|
||
|
||
func sheetPositiveSize(cmd *cobra.Command, required bool) (int, int, error) {
|
||
width, _ := cmd.Flags().GetInt("width")
|
||
height, _ := cmd.Flags().GetInt("height")
|
||
if required || cmd.Flags().Changed("width") {
|
||
if width <= 0 {
|
||
return 0, 0, apperrors.NewValidation("--width must be > 0")
|
||
}
|
||
}
|
||
if required || cmd.Flags().Changed("height") {
|
||
if height <= 0 {
|
||
return 0, 0, apperrors.NewValidation("--height must be > 0")
|
||
}
|
||
}
|
||
return width, height, nil
|
||
}
|
||
|
||
func sheetCondFormatMutationBase(cmd *cobra.Command, update bool) (map[string]any, error) {
|
||
params, err := sheetBaseParams(cmd)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if update {
|
||
ruleID, err := sheetRequiredFlag(cmd, "rule-id")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
params["ruleId"] = ruleID
|
||
}
|
||
if cmd.Flags().Changed("ranges") || !update {
|
||
var ranges []string
|
||
if err := sheetParseJSONFlag(cmd, "ranges", &ranges); err != nil {
|
||
return nil, err
|
||
}
|
||
if len(ranges) == 0 {
|
||
return nil, apperrors.NewValidation("--ranges must contain at least one range")
|
||
}
|
||
params["ranges"] = ranges
|
||
}
|
||
if cmd.Flags().Changed("condition") || !update {
|
||
var condition map[string]any
|
||
if err := sheetParseJSONFlag(cmd, "condition", &condition); err != nil {
|
||
return nil, err
|
||
}
|
||
for k, v := range condition {
|
||
params[k] = v
|
||
}
|
||
}
|
||
if cmd.Flags().Changed("cell-style") {
|
||
var cellStyle map[string]any
|
||
if err := sheetParseJSONFlag(cmd, "cell-style", &cellStyle); err != nil {
|
||
return nil, err
|
||
}
|
||
params["cellStyle"] = cellStyle
|
||
}
|
||
if cmd.Flags().Changed("data-bar-style") {
|
||
var dataBarStyle map[string]any
|
||
if err := sheetParseJSONFlag(cmd, "data-bar-style", &dataBarStyle); err != nil {
|
||
return nil, err
|
||
}
|
||
params["dataBarStyle"] = dataBarStyle
|
||
}
|
||
return params, nil
|
||
}
|
||
|
||
func runSheetAttachmentUpload(cmd *cobra.Command, runner executor.Runner, writeImage bool) error {
|
||
node, err := sheetRequiredFlagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
filePath, err := sheetRequiredFlag(cmd, "file")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if writeImage {
|
||
if err := sheetValidateRequired(cmd, "sheet-id", "range"); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
info, err := os.Stat(filePath)
|
||
if err != nil {
|
||
return fmt.Errorf("cannot read file %s: %w", filePath, err)
|
||
}
|
||
if info.IsDir() {
|
||
return apperrors.NewValidation(filePath + " is a directory")
|
||
}
|
||
fileName := sheetStringFlag(cmd, "name")
|
||
if fileName == "" {
|
||
fileName = filepath.Base(filePath)
|
||
} else if filepath.Ext(fileName) == "" {
|
||
if ext := filepath.Ext(filePath); ext != "" {
|
||
fileName += ext
|
||
}
|
||
}
|
||
mimeType := sheetStringFlag(cmd, "mime-type")
|
||
if mimeType == "" {
|
||
mimeType = detectMIME(fileName)
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": node,
|
||
"fileName": fileName,
|
||
"fileSize": float64(info.Size()),
|
||
"mimeType": mimeType,
|
||
}
|
||
if commandDryRun(cmd) {
|
||
return runSheetDocTool(cmd, runner, "get_doc_attachment_upload_info", params)
|
||
}
|
||
credResult, err := sheetInvocationResult(cmd, runner, "doc", "get_doc_attachment_upload_info", params)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
uploadURL, resourceID, resourceURL, err := extractDocAttachmentUploadInfo(credResult.Response)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := sheetHTTPPut(cmd, uploadURL, filePath, info.Size(), mimeType); err != nil {
|
||
return err
|
||
}
|
||
if !writeImage {
|
||
return writeCommandPayload(cmd, map[string]any{
|
||
"resourceId": resourceID,
|
||
"resourceUrl": resourceURL,
|
||
"fileName": fileName,
|
||
"mimeType": mimeType,
|
||
"fileSize": info.Size(),
|
||
})
|
||
}
|
||
writeArgs := map[string]any{
|
||
"nodeId": node,
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"rangeAddress": sheetStringFlag(cmd, "range"),
|
||
"resourceId": resourceID,
|
||
"resourceUrl": resourceURL,
|
||
}
|
||
sheetAddChangedIntParam(cmd, writeArgs, "width", "width")
|
||
sheetAddChangedIntParam(cmd, writeArgs, "height", "height")
|
||
return runSheetTool(cmd, runner, "write_image", writeArgs)
|
||
}
|
||
|
||
func runSheetDocTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error {
|
||
result, err := sheetInvocationResult(cmd, runner, "doc", tool, params)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return writeCommandPayload(cmd, result)
|
||
}
|
||
|
||
func sheetHTTPPut(cmd *cobra.Command, uploadURL, filePath string, size int64, mimeType string) error {
|
||
f, err := os.Open(filePath)
|
||
if err != nil {
|
||
return fmt.Errorf("open file failed: %w", err)
|
||
}
|
||
defer f.Close()
|
||
req, err := http.NewRequestWithContext(cmd.Context(), http.MethodPut, uploadURL, f)
|
||
if err != nil {
|
||
return fmt.Errorf("build upload request failed: %w", err)
|
||
}
|
||
req.ContentLength = size
|
||
if mimeType != "" {
|
||
req.Header.Set("Content-Type", mimeType)
|
||
}
|
||
resp, err := (&http.Client{Timeout: 5 * time.Minute}).Do(req)
|
||
if err != nil {
|
||
return fmt.Errorf("OSS upload failed: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||
return fmt.Errorf("OSS upload failed HTTP %d: %s", resp.StatusCode, string(body))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func sheetHTTPGet(cmd *cobra.Command, rawURL, outputPath string) error {
|
||
req, err := http.NewRequestWithContext(cmd.Context(), http.MethodGet, rawURL, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
resp, err := (&http.Client{Timeout: 10 * time.Minute}).Do(req)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||
return fmt.Errorf("download failed HTTP %d: %s", resp.StatusCode, string(body))
|
||
}
|
||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||
return err
|
||
}
|
||
f, err := os.Create(outputPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer f.Close()
|
||
_, err = io.Copy(f, resp.Body)
|
||
return err
|
||
}
|
||
|
||
func pollSheetExport(cmd *cobra.Command, runner executor.Runner, jobID string) (downloadURL, status string, err error) {
|
||
intervals := make([]time.Duration, 0, 30)
|
||
for i := 0; i < 5; i++ {
|
||
intervals = append(intervals, 2*time.Second)
|
||
}
|
||
for i := 0; i < 5; i++ {
|
||
intervals = append(intervals, 5*time.Second)
|
||
}
|
||
for i := 0; i < 10; i++ {
|
||
intervals = append(intervals, 10*time.Second)
|
||
}
|
||
for i := 0; i < 10; i++ {
|
||
intervals = append(intervals, 15*time.Second)
|
||
}
|
||
for _, wait := range intervals {
|
||
timer := time.NewTimer(wait)
|
||
select {
|
||
case <-cmd.Context().Done():
|
||
timer.Stop()
|
||
return "", "", cmd.Context().Err()
|
||
case <-timer.C:
|
||
}
|
||
result, err := sheetInvocationResult(cmd, runner, "sheet", "query_export_job", map[string]any{"jobId": jobID})
|
||
if err != nil {
|
||
continue
|
||
}
|
||
status = strings.ToUpper(strings.TrimSpace(sheetStringFromResponse(result.Response, "status")))
|
||
downloadURL = sheetStringFromResponse(result.Response, "downloadUrl")
|
||
switch status {
|
||
case "SUCCESS":
|
||
return downloadURL, status, nil
|
||
case "FAILED", "FAIL", "ERROR":
|
||
msg := sheetStringFromResponse(result.Response, "message")
|
||
if msg == "" {
|
||
msg = "export failed"
|
||
}
|
||
return "", status, apperrors.NewValidation(msg)
|
||
}
|
||
}
|
||
return "", status, apperrors.NewValidation("export timed out")
|
||
}
|
||
|
||
func sheetStringFromResponse(resp map[string]any, key string) string {
|
||
data := sheetResponseData(resp)
|
||
if v, ok := data[key].(string); ok {
|
||
return v
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func sheetResponseData(resp map[string]any) map[string]any {
|
||
if resp == nil {
|
||
return map[string]any{}
|
||
}
|
||
data := resp
|
||
for {
|
||
next, ok := data["result"].(map[string]any)
|
||
if !ok {
|
||
next, ok = data["data"].(map[string]any)
|
||
}
|
||
if !ok {
|
||
next, ok = data["content"].(map[string]any)
|
||
}
|
||
if !ok || len(next) == 0 {
|
||
return data
|
||
}
|
||
data = next
|
||
}
|
||
}
|
||
|
||
func sheetResultFilterViews(resp map[string]any) []map[string]any {
|
||
data := sheetResponseData(resp)
|
||
raw, _ := data["filterViews"].([]any)
|
||
out := make([]map[string]any, 0, len(raw))
|
||
for _, item := range raw {
|
||
if m, ok := item.(map[string]any); ok {
|
||
out = append(out, m)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func sheetFindFilterView(views []map[string]any, id string) (map[string]any, error) {
|
||
for _, view := range views {
|
||
if v, _ := view["id"].(string); v == id {
|
||
return view, nil
|
||
}
|
||
if v, _ := view["filterViewId"].(string); v == id {
|
||
return view, nil
|
||
}
|
||
}
|
||
return nil, apperrors.NewValidation("filter view not found: " + id)
|
||
}
|
||
|
||
func sheetExportFilename(downloadURL, jobID string) string {
|
||
if parsed, err := url.Parse(downloadURL); err == nil {
|
||
name := filepath.Base(parsed.Path)
|
||
if decoded, decodeErr := url.PathUnescape(name); decodeErr == nil && decoded != "" {
|
||
name = decoded
|
||
}
|
||
name = strings.ReplaceAll(name, "\\", "/")
|
||
name = filepath.Base(name)
|
||
if name != "" && name != "." && name != "/" {
|
||
return name
|
||
}
|
||
}
|
||
return "sheet-export-" + jobID + ".xlsx"
|
||
}
|
||
|
||
type sheetStyleSpec struct {
|
||
BgColor string `json:"bgColor,omitempty"`
|
||
BgColorsJSON string `json:"bgColorsJson,omitempty"`
|
||
FontSize int `json:"fontSize,omitempty"`
|
||
FontSizesJSON string `json:"fontSizesJson,omitempty"`
|
||
HAlign string `json:"hAlign,omitempty"`
|
||
HAlignsJSON string `json:"hAlignsJson,omitempty"`
|
||
VAlign string `json:"vAlign,omitempty"`
|
||
VAlignsJSON string `json:"vAlignsJson,omitempty"`
|
||
FontColor string `json:"fontColor,omitempty"`
|
||
FontColorsJSON string `json:"fontColorsJson,omitempty"`
|
||
FontWeight string `json:"fontWeight,omitempty"`
|
||
FontWeightsJSON string `json:"fontWeightsJson,omitempty"`
|
||
WordWrap string `json:"wordWrap,omitempty"`
|
||
NumberFormat string `json:"numberFormat,omitempty"`
|
||
}
|
||
|
||
type sheetBatchStyleItem struct {
|
||
SheetID string `json:"sheetId"`
|
||
Range string `json:"range"`
|
||
sheetStyleSpec
|
||
}
|
||
|
||
func newSheetRangeSetStyleCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("set-style", "设置指定单元格区域的样式", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "sheet-id", "range"); err != nil {
|
||
return err
|
||
}
|
||
rows, cols, err := sheetParseA1Range(sheetStringFlag(cmd, "range"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]any{
|
||
"nodeId": sheetStringFlag(cmd, "node"),
|
||
"sheetId": sheetStringFlag(cmd, "sheet-id"),
|
||
"rangeAddress": sheetStringFlag(cmd, "range"),
|
||
}
|
||
if err := sheetApplyStyleSpec(sheetReadStyleSpec(cmd), rows, cols, params); err != nil {
|
||
return err
|
||
}
|
||
return runSheetTool(cmd, runner, "update_range", params)
|
||
})
|
||
addSheetBaseFlags(cmd)
|
||
cmd.Flags().String("range", "", "目标单元格区域地址 (必填)")
|
||
bindSheetStyleFlags(cmd)
|
||
return cmd
|
||
}
|
||
|
||
func newSheetRangeBatchSetStyleCommand(runner executor.Runner) *cobra.Command {
|
||
cmd := newSheetLeaf("batch-set-style", "按配置文件批量设置样式", func(cmd *cobra.Command, _ []string) error {
|
||
if err := sheetValidateRequired(cmd, "node", "batch"); err != nil {
|
||
return err
|
||
}
|
||
data, err := os.ReadFile(sheetStringFlag(cmd, "batch"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
var items []sheetBatchStyleItem
|
||
if err := json.Unmarshal(data, &items); err != nil {
|
||
return err
|
||
}
|
||
if len(items) == 0 {
|
||
return apperrors.NewValidation("--batch 配置为空")
|
||
}
|
||
continueOnErr, _ := cmd.Flags().GetBool("continue-on-error")
|
||
total := len(items)
|
||
var outputs []executor.Result
|
||
var firstErr error
|
||
failed := 0
|
||
// 失败语义:默认遇错即停;--continue-on-error 跳过坏条目继续,
|
||
// 跑完后仍统一返回首个错误(与 wukong 对齐,避免静默吞错)。
|
||
recordErr := func(err error) bool {
|
||
failed++
|
||
if firstErr == nil {
|
||
firstErr = err
|
||
}
|
||
return continueOnErr
|
||
}
|
||
for _, item := range items {
|
||
if item.SheetID == "" || item.Range == "" {
|
||
if !recordErr(apperrors.NewValidation("batch item requires sheetId and range")) {
|
||
return firstErr
|
||
}
|
||
continue
|
||
}
|
||
rows, cols, err := sheetParseA1Range(item.Range)
|
||
if err != nil {
|
||
if !recordErr(err) {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
params := map[string]any{"nodeId": sheetStringFlag(cmd, "node"), "sheetId": item.SheetID, "rangeAddress": item.Range}
|
||
if err := sheetApplyStyleSpec(&item.sheetStyleSpec, rows, cols, params); err != nil {
|
||
if !recordErr(err) {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
result, err := sheetInvocationResult(cmd, runner, "sheet", "update_range", params)
|
||
if err != nil {
|
||
if !recordErr(err) {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
outputs = append(outputs, result)
|
||
}
|
||
if firstErr != nil {
|
||
return apperrors.NewValidation(fmt.Sprintf("batch-set-style 共 %d 条失败(首个错误:%v)", failed, firstErr))
|
||
}
|
||
return writeCommandPayload(cmd, map[string]any{"count": len(outputs), "total": total, "results": outputs})
|
||
})
|
||
addSheetNodeFlags(cmd)
|
||
cmd.Flags().String("batch", "", "批次配置 JSON 文件路径 (必填)")
|
||
cmd.Flags().Bool("continue-on-error", false, "遇到失败时继续执行")
|
||
return cmd
|
||
}
|
||
|
||
func bindSheetStyleFlags(cmd *cobra.Command) {
|
||
cmd.Flags().String("bg-color", "", "背景色")
|
||
cmd.Flags().String("bg-colors-json", "", "背景色二维 JSON 数组")
|
||
cmd.Flags().Int("font-size", 0, "字号")
|
||
cmd.Flags().String("font-sizes-json", "", "字号二维 JSON 数组")
|
||
cmd.Flags().String("h-align", "", "水平对齐")
|
||
cmd.Flags().String("h-aligns-json", "", "水平对齐二维 JSON 数组")
|
||
cmd.Flags().String("v-align", "", "垂直对齐")
|
||
cmd.Flags().String("v-aligns-json", "", "垂直对齐二维 JSON 数组")
|
||
cmd.Flags().String("font-color", "", "字体颜色")
|
||
cmd.Flags().String("font-colors-json", "", "字体颜色二维 JSON 数组")
|
||
cmd.Flags().String("font-weight", "", "字体粗细")
|
||
cmd.Flags().String("font-weights-json", "", "字体粗细二维 JSON 数组")
|
||
cmd.Flags().String("word-wrap", "", "换行方式")
|
||
cmd.Flags().String("number-format", "", "数字格式 code")
|
||
}
|
||
|
||
func sheetReadStyleSpec(cmd *cobra.Command) *sheetStyleSpec {
|
||
spec := &sheetStyleSpec{}
|
||
spec.BgColor, _ = cmd.Flags().GetString("bg-color")
|
||
spec.BgColorsJSON, _ = cmd.Flags().GetString("bg-colors-json")
|
||
spec.FontSize, _ = cmd.Flags().GetInt("font-size")
|
||
spec.FontSizesJSON, _ = cmd.Flags().GetString("font-sizes-json")
|
||
spec.HAlign, _ = cmd.Flags().GetString("h-align")
|
||
spec.HAlignsJSON, _ = cmd.Flags().GetString("h-aligns-json")
|
||
spec.VAlign, _ = cmd.Flags().GetString("v-align")
|
||
spec.VAlignsJSON, _ = cmd.Flags().GetString("v-aligns-json")
|
||
spec.FontColor, _ = cmd.Flags().GetString("font-color")
|
||
spec.FontColorsJSON, _ = cmd.Flags().GetString("font-colors-json")
|
||
spec.FontWeight, _ = cmd.Flags().GetString("font-weight")
|
||
spec.FontWeightsJSON, _ = cmd.Flags().GetString("font-weights-json")
|
||
spec.WordWrap, _ = cmd.Flags().GetString("word-wrap")
|
||
spec.NumberFormat, _ = cmd.Flags().GetString("number-format")
|
||
return spec
|
||
}
|
||
|
||
func sheetApplyStyleSpec(spec *sheetStyleSpec, rows, cols int, params map[string]any) error {
|
||
if rows > 1000 || rows*cols > 30000 {
|
||
return apperrors.NewValidation("style range is too large")
|
||
}
|
||
if err := sheetApply2DString(spec.BgColor, spec.BgColorsJSON, rows, cols, "bg-color", "backgroundColors", nil, params); err != nil {
|
||
return err
|
||
}
|
||
if err := sheetApplyFontSize(spec, rows, cols, params); err != nil {
|
||
return err
|
||
}
|
||
if err := sheetApply2DString(spec.HAlign, spec.HAlignsJSON, rows, cols, "h-align", "horizontalAlignments", map[string]bool{"left": true, "center": true, "right": true, "general": true}, params); err != nil {
|
||
return err
|
||
}
|
||
if err := sheetApply2DString(spec.VAlign, spec.VAlignsJSON, rows, cols, "v-align", "verticalAlignments", map[string]bool{"top": true, "middle": true, "bottom": true}, params); err != nil {
|
||
return err
|
||
}
|
||
if err := sheetApply2DString(spec.FontColor, spec.FontColorsJSON, rows, cols, "font-color", "fontColors", nil, params); err != nil {
|
||
return err
|
||
}
|
||
if err := sheetApply2DString(spec.FontWeight, spec.FontWeightsJSON, rows, cols, "font-weight", "fontWeights", map[string]bool{"bold": true, "normal": true}, params); err != nil {
|
||
return err
|
||
}
|
||
if spec.WordWrap != "" {
|
||
if !map[string]bool{"overflow": true, "clip": true, "autoWrap": true}[spec.WordWrap] {
|
||
return apperrors.NewValidation("--word-wrap has invalid value")
|
||
}
|
||
params["wordWrap"] = spec.WordWrap
|
||
}
|
||
if spec.NumberFormat != "" {
|
||
params["numberFormat"] = spec.NumberFormat
|
||
}
|
||
for _, key := range []string{"backgroundColors", "fontSizes", "horizontalAlignments", "verticalAlignments", "fontColors", "fontWeights", "wordWrap", "numberFormat"} {
|
||
if _, ok := params[key]; ok {
|
||
return nil
|
||
}
|
||
}
|
||
return apperrors.NewValidation("at least one style flag is required")
|
||
}
|
||
|
||
func sheetApplyFontSize(spec *sheetStyleSpec, rows, cols int, params map[string]any) error {
|
||
if spec.FontSize != 0 && spec.FontSizesJSON != "" {
|
||
return apperrors.NewValidation("--font-size and --font-sizes-json are mutually exclusive")
|
||
}
|
||
if spec.FontSize != 0 {
|
||
if spec.FontSize < 0 {
|
||
return apperrors.NewValidation("--font-size must be positive")
|
||
}
|
||
params["fontSizes"] = sheetFillIntMatrix(rows, cols, spec.FontSize)
|
||
return nil
|
||
}
|
||
if spec.FontSizesJSON != "" {
|
||
var m [][]int
|
||
if err := json.Unmarshal([]byte(spec.FontSizesJSON), &m); err != nil {
|
||
return err
|
||
}
|
||
if !sheetMatrixIntShape(m, rows, cols) {
|
||
return apperrors.NewValidation("--font-sizes-json shape does not match range")
|
||
}
|
||
params["fontSizes"] = m
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func sheetApply2DString(scalar, jsonStr string, rows, cols int, flagName, key string, enum map[string]bool, params map[string]any) error {
|
||
if scalar != "" && jsonStr != "" {
|
||
return apperrors.NewValidation("--" + flagName + " and --" + flagName + "s-json are mutually exclusive")
|
||
}
|
||
if scalar != "" {
|
||
if enum != nil && !enum[scalar] {
|
||
return apperrors.NewValidation("--" + flagName + " has invalid value")
|
||
}
|
||
params[key] = sheetFillStringMatrix(rows, cols, scalar)
|
||
return nil
|
||
}
|
||
if jsonStr == "" {
|
||
return nil
|
||
}
|
||
var m [][]string
|
||
if err := json.Unmarshal([]byte(jsonStr), &m); err != nil {
|
||
return err
|
||
}
|
||
if !sheetMatrixStringShape(m, rows, cols) {
|
||
return apperrors.NewValidation(fmt.Sprintf("--%s-json 行列维度与 --range 不一致 (dimension does not match range)", flagName))
|
||
}
|
||
if enum != nil {
|
||
for _, row := range m {
|
||
for _, v := range row {
|
||
if v != "" && !enum[v] {
|
||
return apperrors.NewValidation("--" + flagName + "-json has invalid value")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
params[key] = m
|
||
return nil
|
||
}
|
||
|
||
func sheetParseA1Range(addr string) (rows, cols int, err error) {
|
||
if i := strings.Index(addr, "!"); i >= 0 {
|
||
addr = addr[i+1:]
|
||
}
|
||
addr = strings.TrimSpace(strings.ToUpper(addr))
|
||
parts := strings.SplitN(addr, ":", 2)
|
||
c1, r1, err := sheetParseA1Cell(parts[0])
|
||
if err != nil {
|
||
return 0, 0, err
|
||
}
|
||
c2, r2 := c1, r1
|
||
if len(parts) == 2 {
|
||
c2, r2, err = sheetParseA1Cell(parts[1])
|
||
if err != nil {
|
||
return 0, 0, err
|
||
}
|
||
}
|
||
if c2 < c1 {
|
||
c1, c2 = c2, c1
|
||
}
|
||
if r2 < r1 {
|
||
r1, r2 = r2, r1
|
||
}
|
||
return r2 - r1 + 1, c2 - c1 + 1, nil
|
||
}
|
||
|
||
func sheetParseA1Cell(s string) (col, row int, err error) {
|
||
for len(s) > 0 && s[0] >= 'A' && s[0] <= 'Z' {
|
||
col = col*26 + int(s[0]-'A'+1)
|
||
s = s[1:]
|
||
}
|
||
if col == 0 || s == "" {
|
||
return 0, 0, apperrors.NewValidation("invalid A1 cell")
|
||
}
|
||
row, err = strconv.Atoi(s)
|
||
if err != nil || row <= 0 {
|
||
return 0, 0, apperrors.NewValidation("invalid A1 cell")
|
||
}
|
||
return col, row, nil
|
||
}
|
||
|
||
func sheetFillStringMatrix(rows, cols int, v string) [][]string {
|
||
out := make([][]string, rows)
|
||
for i := range out {
|
||
out[i] = make([]string, cols)
|
||
for j := range out[i] {
|
||
out[i][j] = v
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func sheetFillIntMatrix(rows, cols, v int) [][]int {
|
||
out := make([][]int, rows)
|
||
for i := range out {
|
||
out[i] = make([]int, cols)
|
||
for j := range out[i] {
|
||
out[i][j] = v
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func sheetMatrixStringShape(m [][]string, rows, cols int) bool {
|
||
if len(m) != rows {
|
||
return false
|
||
}
|
||
for _, row := range m {
|
||
if len(row) != cols {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func sheetMatrixIntShape(m [][]int, rows, cols int) bool {
|
||
if len(m) != rows {
|
||
return false
|
||
}
|
||
for _, row := range m {
|
||
if len(row) != cols {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|