Files
dingtalk-workspace-cli/internal/helpers/sheet_data.go
T

297 lines
13 KiB
Go

package helpers
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/spf13/cobra"
)
func newDataCmds() []*cobra.Command {
findCmd := &cobra.Command{
Use: "find",
Short: "在工作表中搜索单元格内容",
Long: `在钉钉电子表格的指定工作表中查找匹配指定文本的所有单元格,返回匹配的单元格地址和值。
通过 --node 定位电子表格文档,通过 --sheet-id 定位工作表,通过 --query 指定查找内容(--find 为其别名)。
nodeId 支持传入文档链接 URL 或文档 ID(dentryUuid),系统自动识别。
sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获取。
支持多种查找模式:
默认模式 在单元格值中进行子字符串匹配(区分大小写)
忽略大小写 通过 --match-case=false 实现
完整单元格匹配 通过 --match-entire-cell 实现
正则表达式 通过 --use-regexp 实现,--query 作为正则表达式匹配
搜索公式文本 通过 --match-formula 实现,在公式文本中查找而非计算结果
包含隐藏单元格 通过 --include-hidden 实现
可通过 --range 限定搜索范围(A1 表示法),不传时搜索整个工作表。`,
Example: ` # 基本搜索
dws sheet find --node NODE_ID --sheet-id SHEET_ID --query "销售额"
# 在指定范围内搜索
dws sheet find --node NODE_ID --sheet-id SHEET_ID --query "合计" --range "A1:D100"
# 正则表达式搜索(不区分大小写)
dws sheet find --node NODE_ID --sheet-id SHEET_ID --query "^total" --use-regexp --match-case=false
# 精确匹配整个单元格内容
dws sheet find --node NODE_ID --sheet-id SHEET_ID --query "完成" --match-entire-cell
# 搜索公式文本(使用 --find 别名)
dws sheet find --node NODE_ID --sheet-id SHEET_ID --find "SUM" --match-formula`,
RunE: func(cmd *cobra.Command, args []string) error {
text, err := mustFlagOrFallback(cmd, "query", "find")
if err != nil {
return err
}
toolArgs := map[string]any{
"nodeId": mustGetFlag(cmd, "node"),
"sheetId": mustGetFlag(cmd, "sheet-id"),
"text": text,
}
if v, _ := cmd.Flags().GetString("range"); v != "" {
toolArgs["range"] = v
}
matchCase, _ := cmd.Flags().GetBool("match-case")
toolArgs["matchCase"] = matchCase
matchEntireCell, _ := cmd.Flags().GetBool("match-entire-cell")
toolArgs["matchEntireCell"] = matchEntireCell
useRegExp, _ := cmd.Flags().GetBool("use-regexp")
toolArgs["useRegExp"] = useRegExp
matchFormula, _ := cmd.Flags().GetBool("match-formula")
toolArgs["matchFormulaText"] = matchFormula
includeHidden, _ := cmd.Flags().GetBool("include-hidden")
toolArgs["includeHidden"] = includeHidden
return callMCPTool("find_cells", toolArgs)
},
}
findCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
findCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
findCmd.Flags().String("query", "", "搜索文本 (必填,别名: --find)")
findCmd.Flags().String("find", "", "--query 的别名")
_ = findCmd.Flags().MarkHidden("find")
findCmd.Flags().String("range", "", "搜索范围,A1 表示法 (如 A1:D10)")
findCmd.Flags().Bool("match-case", true, "区分大小写 (默认 true)")
findCmd.Flags().Bool("match-entire-cell", false, "精确匹配整个单元格内容")
findCmd.Flags().Bool("use-regexp", false, "启用正则表达式搜索")
findCmd.Flags().Bool("match-formula", false, "搜索公式文本而非显示值")
findCmd.Flags().Bool("include-hidden", false, "包含隐藏单元格")
replaceCmd := &cobra.Command{
Use: "replace",
Short: "查找替换/批量替换/精确匹配替换/正则替换文本",
Long: `在钉钉表格的指定工作表中,全局查找并替换文本内容。
支持以下替换选项:
--match-case 区分大小写(默认 false)
--match-entire-cell 完整单元格匹配(默认 false)
--use-regexp 使用正则表达式匹配(默认 false)
--include-hidden 包含隐藏行/列(默认 false)
--range 限定替换范围,A1 表示法(不传时在整个工作表中替换)
返回被替换的单元格数量。
nodeId 支持传入文档链接 URL 或文档 ID(dentryUuid),系统自动识别。
sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获取。`,
Example: ` # 基本替换
dws sheet replace --node NODE_ID --sheet-id SHEET_ID --find "旧文本" --replacement "新文本"
# 精确匹配整个单元格后替换
dws sheet replace --node NODE_ID --sheet-id SHEET_ID --find "待处理" --replacement "已完成" --match-entire-cell
# 正则表达式替换
dws sheet replace --node NODE_ID --sheet-id SHEET_ID --find "\\d{4}" --replacement "****" --use-regexp
# 在指定范围内替换
dws sheet replace --node NODE_ID --sheet-id SHEET_ID --find "旧" --replacement "新" --range "A1:D100"
# 删除匹配内容(替换为空)
dws sheet replace --node NODE_ID --sheet-id SHEET_ID --find "临时" --replacement ""`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs := map[string]any{
"nodeId": mustGetFlag(cmd, "node"),
"sheetId": mustGetFlag(cmd, "sheet-id"),
"text": mustGetFlag(cmd, "find"),
"replaceText": mustGetFlag(cmd, "replacement"),
}
if v, _ := cmd.Flags().GetString("range"); v != "" {
toolArgs["range"] = v
}
matchCase, _ := cmd.Flags().GetBool("match-case")
toolArgs["matchCase"] = matchCase
matchEntireCell, _ := cmd.Flags().GetBool("match-entire-cell")
toolArgs["matchEntireCell"] = matchEntireCell
useRegExp, _ := cmd.Flags().GetBool("use-regexp")
toolArgs["useRegExp"] = useRegExp
includeHidden, _ := cmd.Flags().GetBool("include-hidden")
toolArgs["includeHidden"] = includeHidden
return callMCPTool("replace_all", toolArgs)
},
}
replaceCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
replaceCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
replaceCmd.Flags().String("find", "", "查找文本 (必填)")
replaceCmd.Flags().String("replacement", "", "替换文本 (必填,可为空字符串表示删除)")
replaceCmd.Flags().String("range", "", "替换范围,A1 表示法 (如 A1:D100)")
replaceCmd.Flags().Bool("match-case", false, "区分大小写 (默认 false)")
replaceCmd.Flags().Bool("match-entire-cell", false, "完整单元格匹配")
replaceCmd.Flags().Bool("use-regexp", false, "启用正则表达式匹配")
replaceCmd.Flags().Bool("include-hidden", false, "包含隐藏行/列")
appendCmd := &cobra.Command{
Use: "append",
Short: "在工作表末尾追加数据",
Long: `在钉钉电子表格的指定工作表末尾追加若干行数据。
系统会自动定位到工作表中最后一行有数据的位置,在其下方插入新行数据。
如果工作表为空,数据将从第一行开始写入。
--values 为二维 JSON 数组,外层数组的每个元素代表一行,内层数组的每个元素代表该行中的一个单元格值。
追加的数据列数应与工作表已有数据的列数保持一致,以确保数据对齐。
nodeId 支持传入文档链接 URL 或文档 ID(dentryUuid),系统自动识别。
sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获取。`,
Example: ` dws sheet append --node NODE_ID --sheet-id SHEET_ID --values '[["张三","销售部",50000]]'
dws sheet append --node NODE_ID --sheet-id SHEET_ID \
--values '[["李四","市场部",38000],["王五","销售部",62000]]'`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "node", "sheet-id", "values"); err != nil {
return err
}
var values [][]any
if err := json.Unmarshal([]byte(mustGetFlag(cmd, "values")), &values); err != nil {
return fmt.Errorf("--values JSON 解析失败: %w", err)
}
return callMCPTool("append_rows", map[string]any{
"nodeId": mustGetFlag(cmd, "node"),
"sheetId": mustGetFlag(cmd, "sheet-id"),
"values": values,
})
},
}
appendCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
appendCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
appendCmd.Flags().String("values", "", "追加数据,二维 JSON 数组 (必填)")
csvPutCmd := &cobra.Command{
Use: "csv-put",
Short: "将 CSV 数据写入表格指定位置(纯值,自动扩容)",
Long: `将 RFC 4180 格式的 CSV 文本写入指定工作表的指定单元格位置。
只写纯值,不支持公式/样式/批注。=开头当文本处理。自动扩容行列。
目标区域如含合并单元格,csv-put 会打散合并并覆盖所有单元格;这不同于
range update 与合并区域冲突时返回 MERGED_CELLS_CONFLICT 的行为。
--csv 支持三种输入:直接传文本、@filepath 从文件读取、- 从 stdin 读取。
--allow-overwrite 默认 false,目标区域有数据时需显式传 --allow-overwrite 才能覆盖。`,
Example: ` dws sheet csv-put --node NODE_ID --sheet-id SHEET_ID --start-cell A1 \
--csv 'name,score\nalice,95\nbob,87'
dws sheet csv-put --node NODE_ID --sheet-id SHEET_ID --start-cell B2 \
--csv @data.csv --allow-overwrite
cat data.csv | dws sheet csv-put --node NODE_ID --sheet-id SHEET_ID \
--start-cell A1 --csv -
dws sheet csv-put --node NODE_ID --sheet-id SHEET_ID --start-cell A1 \
--csv @data.csv --dry-run`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "node", "sheet-id", "csv", "start-cell"); err != nil {
return err
}
csvContent := mustGetFlag(cmd, "csv")
switch {
case csvContent == "-":
data, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("读取 stdin 失败: %w", err)
}
csvContent = string(data)
case strings.HasPrefix(csvContent, "@"):
data, err := os.ReadFile(strings.TrimPrefix(csvContent, "@"))
if err != nil {
return fmt.Errorf("读取 CSV 文件失败: %w", err)
}
csvContent = string(data)
}
csvContent = strings.ReplaceAll(csvContent, "\r", "")
csvContent = strings.TrimPrefix(csvContent, "\xef\xbb\xbf")
toolArgs := map[string]any{
"nodeId": mustGetFlag(cmd, "node"),
"sheetId": mustGetFlag(cmd, "sheet-id"),
"csv": csvContent,
"startCell": mustGetFlag(cmd, "start-cell"),
}
if cmd.Flags().Changed("allow-overwrite") {
v, _ := cmd.Flags().GetBool("allow-overwrite")
toolArgs["allowOverwrite"] = v
}
return callMCPTool("set_range_from_csv", toolArgs)
},
}
csvPutCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
csvPutCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
csvPutCmd.Flags().String("csv", "", "CSV 文本、@文件路径 或 - 表示 stdin (必填)")
csvPutCmd.Flags().String("start-cell", "", "起始单元格,A1 表示法 (必填)")
csvPutCmd.Flags().Bool("allow-overwrite", false, "允许覆盖已有数据 (默认 false)")
csvGetCmd := &cobra.Command{
Use: "csv-get",
Short: "以 CSV 格式读取工作表数据",
Long: `以 RFC 4180 CSV 格式读取钉钉电子表格数据。
返回内容:
csv 带 [row=N] 行号前缀的 CSV 文本
colIndices 列字母映射数组(定位列用 colIndices[j],禁止手数逗号)
rowIndices 行号映射数组
hasMore 是否因 maxChars 截断
取值模式(--value-render-option):
formatted_value 格式化后的展示值(默认),如 ¥1,000.00、2025-06-01
raw_value 原始值,如 1000、45808
formula 公式文本,如 =SUM(A1:A10),无公式时回退原始值
与 range read 的区别:
- CSV 格式 token 消耗约为 JSON 的 1/3
- 支持选择取值模式
- 自动防爆(max-chars 截断 + has_more 标志)
- [row=N] 前缀防止行号计算错误
注意:csv-get 不返回合并单元格结构。查看合并范围请使用
dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 mergedRanges。`,
Example: ` dws sheet csv-get --node NODE_ID
dws sheet csv-get --node NODE_ID --sheet-id SHEET_ID --range "A1:D10"
dws sheet csv-get --node NODE_ID --range "A1:Z500" --value-render-option raw_value
dws sheet csv-get --node NODE_ID --range "A1:D10" --max-chars 50000`,
RunE: func(cmd *cobra.Command, args []string) error {
toolArgs := map[string]any{
"nodeId": mustGetFlag(cmd, "node"),
}
if v, _ := cmd.Flags().GetString("sheet-id"); v != "" {
toolArgs["sheetId"] = v
}
if v, _ := cmd.Flags().GetString("range"); v != "" {
toolArgs["range"] = v
}
if v, _ := cmd.Flags().GetString("value-render-option"); v != "" {
toolArgs["valueRenderOption"] = v
}
if cmd.Flags().Changed("max-chars") {
v, _ := cmd.Flags().GetInt("max-chars")
toolArgs["maxChars"] = v
}
return callMCPTool("get_range_as_csv", toolArgs)
},
}
csvGetCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
csvGetCmd.Flags().String("sheet-id", "", "工作表 ID 或名称")
csvGetCmd.Flags().String("range", "", "读取范围,A1 表示法 (不传则读取全部非空数据)")
csvGetCmd.Flags().String("value-render-option", "", "取值模式: formatted_value | raw_value | formula")
csvGetCmd.Flags().Int("max-chars", 0, "CSV 最大字符数 (默认 200000)")
return []*cobra.Command{findCmd, replaceCmd, appendCmd, csvPutCmd, csvGetCmd}
}