654 lines
32 KiB
Go
654 lines
32 KiB
Go
package helpers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
// newRangeCmd creates the range parent command with its core children
|
||
// (read/update/clear/sort/fill/copy-to/move-to).
|
||
// set-style, batch-set-style, and batch-clear are added by newSheetCommand().
|
||
func newRangeCmd() *cobra.Command {
|
||
rangeCmd := &cobra.Command{Use: "range", Short: "数据区域操作"}
|
||
|
||
rangeReadCmd := &cobra.Command{
|
||
Use: "read",
|
||
Aliases: []string{"get"},
|
||
Short: "读取工作表数据(别名: get)",
|
||
Long: `读取钉钉电子表格中指定工作表的指定范围数据,返回 per-cell 结构化信息。
|
||
|
||
别名: dws sheet range get 与 dws sheet range read 等价。
|
||
|
||
返回内容为 cells 二维数组,每个 cell 包含:
|
||
value 单元格值(内容由 --value-render-option 决定)
|
||
dataValidation 数据验证配置(下拉列表/复选框),无则为 null
|
||
hyperlink 单元格级超链接(path/sheet/range),无则省略
|
||
顶层还会返回 rowIndices / colIndices 与完成度字段:
|
||
hasMore 目标范围是否还有未返回数据
|
||
truncationReasons 部分返回原因(max_cells)
|
||
resolvedRange 未传 --range 时底层解析出的完整目标范围
|
||
returnedRange 本次实际完整返回的范围
|
||
|
||
单次最多返回 30,000 个单元格。hasMore=true 是部分成功,本命令
|
||
不会自动续读;请结合目标范围和 returnedRange 从下一行显式传
|
||
--range。收到 forbidden.document.sizeOverLimit 则表示工作簿整体无法装载,
|
||
应创建更小副本或拆分工作簿,缩小 --range 不能解决。
|
||
|
||
注意:range read/get 不返回合并单元格结构。查看合并范围请使用
|
||
dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 mergedRanges。
|
||
|
||
取值模式 (--value-render-option):
|
||
formatted_value 格式化后的展示值(默认),如 ¥1,000.00、2025-06-01
|
||
raw_value 原始值,如 1000、45808
|
||
formula 公式文本,如 =SUM(A1:A10),无公式时回退原始值
|
||
|
||
范围格式使用 A1 表示法:
|
||
--range A1:D10 读取当前工作表 A1:D10 区域
|
||
--range "Sheet1!A1:D10" 读取 Sheet1 工作表的 A1:D10 区域(忽略 --sheet-id)
|
||
不传 --range 则默认读取整个工作表的全部非空数据
|
||
|
||
超时处理建议: 读取大范围数据时若出现超时或响应过慢,请主动缩小 --range 查询范围,
|
||
建议单次读取的单元格数量控制在 5000 个以内(例如 50 行 × 100 列、100 行 × 50 列)。
|
||
对于大表可采用分页读取策略:先通过 info 获取 nonEmptyRange.range
|
||
或 nonEmptyRange.lastRow / nonEmptyRange.lastColumn 确定 A1 边界;
|
||
再按行分批读取,避免不传 --range 直接读取整个大工作表。`,
|
||
Example: ` dws sheet range read --node NODE_ID
|
||
dws sheet range read --node NODE_ID --sheet-id SHEET_ID
|
||
dws sheet range read --node NODE_ID --sheet-id "Sheet1" --range "A1:D10"
|
||
dws sheet range read --node NODE_ID --range "Sheet1!A1:D10"
|
||
dws sheet range read --node NODE_ID --value-render-option raw_value
|
||
dws sheet range read --node NODE_ID --value-render-option formula
|
||
|
||
# 使用 get 别名
|
||
dws sheet range get --node NODE_ID --sheet-id SHEET_ID --range "A1:D10"`,
|
||
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
|
||
}
|
||
return callMCPToolCellInfos(toolArgs)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(rangeReadCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "sheet",
|
||
Name: "range_read",
|
||
CanonicalPath: "sheet.range_read",
|
||
CLIPath: "sheet range read",
|
||
PrimaryCLIPath: "sheet range read",
|
||
},
|
||
Description: "读取工作表指定范围的单元格数据(可取格式化值/原始值/公式)。",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed unpinned remote adapter: the CLI calls sheet/get_cell_infos, which is absent from the pinned MCP metadata snapshot; the incompatible sheet/get_range contract must not be advertised.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "读取工作表指定范围的单元格数据(可取格式化值/原始值/公式)。",
|
||
UseWhen: []string{"需要查看 axls 工作表内容或指定 A1 范围时;大表应限制 range"},
|
||
AvoidWhen: []string{"搜索关键字用 sheet find;纯 CSV 文本导出区域用 sheet csv-get;本地 xlsx 用 doc download;AI 表格记录用 aitable record query"},
|
||
Examples: []string{"dws sheet range read --node <NODE_ID> --sheet-id <SHEET_ID> --range \"A1:D10\""},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "node", Property: "nodeId"},
|
||
{Name: "value-render-option", Property: "valueRenderOption"},
|
||
},
|
||
},
|
||
})
|
||
rangeReadCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
|
||
rangeReadCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (不传则默认第一个工作表)")
|
||
rangeReadCmd.Flags().String("range", "", "读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据)")
|
||
rangeReadCmd.Flags().String("value-render-option", "", "取值模式: formatted_value(默认) | raw_value | formula")
|
||
|
||
rangeUpdateCmd := &cobra.Command{
|
||
Use: "update",
|
||
Short: "更新工作表指定区域内容",
|
||
Long: `更新钉钉在线表格指定区域内单元格内容。
|
||
|
||
--values 二维 JSON 数组,行列维度需与 --range 完全一致。每个 cell 是以下之一:
|
||
- {} 空对象:跳过该单元格,保留原值不变
|
||
- {type:"text",...} 或 {type:"richText",...} 对象
|
||
|
||
【type=text】普通文本(可带 cellStyles)
|
||
{"type":"text", "text":"内容"}
|
||
{"type":"text", "text":"内容", "cellStyles":{"fontWeight":"bold","fontColor":"#FF0000"}}
|
||
- text 以 "=" 开头识别为公式(如 "=SUM(B2:B4)")
|
||
- text 为空字符串 "" 表示清空该单元格
|
||
- 写数字 / 布尔请用字符串形式(如 {"type":"text","text":"100"}),
|
||
服务端按内容自动识别为数字 / 布尔
|
||
|
||
【{} 跳过】保留原值不变
|
||
只更新部分单元格时,用 {} 占位不需要修改的位置,避免拆分成多次调用
|
||
|
||
【type=richText】富文本(多子项组合:超链接 / 附件 / 图片 / 带样式片段)
|
||
{"type":"richText", "texts":[...]}
|
||
texts[].type 枚举:
|
||
• text {"type":"text", "text":"文字", "style":{...}?}
|
||
• link {"type":"link", "text":"显示文字","link":"https://...", "subType":"path"?, "style":{...}?}
|
||
• attachment {"type":"attachment", "text":"文件名.pdf","resourceId":"...","mimeType":"application/pdf","size":12345?}
|
||
• image {"type":"image", "text":"", "resourceId":"...","resourceUrl":"/core/api/resources/img/xxx","width":?,"height":?}
|
||
resourceId / resourceUrl 通过 dws sheet media-upload 获取。
|
||
link.subType 可为 path/sheet/range;不传默认 path。sheet/range 必须使用真实工作表名称或 A1 范围,禁止猜 Sheet1。
|
||
|
||
【style 子结构】(仅 richText 的 text / link 子项支持)
|
||
bold / italic / underline / strike: boolean
|
||
color: 16 进制色值字符串如 "#FF0000"
|
||
size: 字号正整数
|
||
|
||
【cellStyles 子结构】(可选,与 type 同级,作用于整格)
|
||
fontWeight/fontColor/fontSize/fontStyle/backgroundColor/horizontalAlignment/verticalAlignment/wordWrap/numberFormat/textUnderline/textLineThrough
|
||
只设样式不改值时,可传 {"cellStyles":{...}} 保留原值;大面积统一样式优先用 dws sheet range set-style。
|
||
|
||
【hyperlink 子结构】(可选,与 type 同级,作用于整格)
|
||
1) 不传 hyperlink 字段 → 保留原整格超链接
|
||
2) {"hyperlink":{"type":"none"}} → 显式清除整格超链接
|
||
3) {"hyperlink":{"type":"path","link":"https://..."}} → 写外部链接
|
||
{"hyperlink":{"type":"sheet","link":"Sheet2"}} → 写工作表链接
|
||
{"hyperlink":{"type":"range","link":"Sheet2!A1"}} → 写区域链接
|
||
|
||
【dataValidation 子结构】(可选,与 type 同级)三种语义:
|
||
1) 不传 dataValidation 字段 → 保留原 DV
|
||
2) {"dataValidation":{"type":"none"}} → 显式清除该单元格 DV
|
||
3) {"dataValidation":{"type":"dropdown",...}} → 写新 dropdown(覆盖)
|
||
{"dataValidation":{"type":"checkbox",...}} → 写新 checkbox(覆盖)
|
||
Inline dropdown:
|
||
{"dataValidation":{"type":"dropdown","options":[{"value":"选项1"}],"enableMultiSelect":false}}
|
||
SourceRange dropdown(同一工作簿内可跨工作表;不展开来源 values,不支持 colors):
|
||
{"dataValidation":{"type":"dropdown","sourceRange":{"sheetId":"SOURCE_SHEET_ID","a1Notation":"T1:T3"},"enableMultiSelect":false}}
|
||
dropdown 的 options 与 sourceRange 必须且只能传一个。SourceRange 支持普通区域、整行和整列;
|
||
a1Notation 不带工作表前缀,来源工作表单独写在 sheetId。
|
||
checkbox: {"dataValidation":{"type":"checkbox","checked":true}}
|
||
可与 text/richText 共存,也可单独使用(如 {dataValidation:{type:"none"}} 仅清除 DV 不写值)
|
||
|
||
注意:
|
||
- 仅支持 text / richText 两种 type;number / boolean / null 等不再支持
|
||
- type=text 顶层旧 style 字段不要作为新写法使用;整格样式用 cellStyles,片段样式用 richText 子项 style
|
||
- 单元格级超链接写在 cell.hyperlink;richText link 仅表示富文本片段链接
|
||
- 写图片到单元格建议直接用 dws sheet write-image(更简洁)
|
||
- 只设样式或批量刷整片区域样式请用 dws sheet range set-style;写值同时设置少量 cell 样式可用 cellStyles
|
||
- 目标范围与已有合并区域冲突时,range update 会返回 MERGED_CELLS_CONFLICT;先用 sheet info 查看 mergedRanges,取消合并后写入,必要时再重新合并
|
||
- csv-put 的合并处理不同:目标区域含合并单元格时会打散合并并写入 CSV 值或公式
|
||
- SourceRange 在已验证的重命名、引用前插入行/列、删除引用前行的场景会自动调整;move-dimension 及其他未覆盖的删除/移动场景后先回读 sourceRangeStatus,仅 invalid 时重新选源写入
|
||
- 同一 cell 的 value/style 已写入但 SourceRange 校验失败时,服务端可返回 success=true,并通过 message 明确下拉未创建;必须检查 message,必要时重新读取确认
|
||
- 清空整片区域请用 dws sheet range clear`,
|
||
Example: ` # 写入文本
|
||
dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1:B2" \
|
||
--values '[[{"type":"text","text":"姓名"},{"type":"text","text":"分数"}],[{"type":"text","text":"张三"},{"type":"text","text":"90"}]]'
|
||
|
||
# 写入公式
|
||
dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "C2" \
|
||
--values '[[{"type":"text","text":"=A2&B2"}]]'
|
||
|
||
# 写入带整格样式的文本(红色加粗)
|
||
dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1" \
|
||
--values '[[{"type":"text","text":"重要","cellStyles":{"fontWeight":"bold","fontColor":"#FF0000"}}]]'
|
||
|
||
# 写入单元格级超链接
|
||
dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1" \
|
||
--values '[[{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]'
|
||
|
||
# 清理单元格级超链接,保留当前值
|
||
dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1" \
|
||
--values '[[{"hyperlink":{"type":"none"}}]]'
|
||
|
||
# 富文本片段链接
|
||
dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1" \
|
||
--values '[[{"type":"richText","texts":[{"type":"link","text":"钉钉","link":"https://dingtalk.com"}]}]]'
|
||
|
||
# 富文本:普通文字 + 带下划线的超链接
|
||
dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1" \
|
||
--values '[[{"type":"richText","texts":[{"type":"text","text":"请访问 "},{"type":"link","text":"官网","link":"https://dingtalk.com","style":{"underline":true,"color":"#0080FF"}}]}]]'
|
||
|
||
# 清空单个单元格(text 为空字符串)
|
||
dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1" \
|
||
--values '[[{"type":"text","text":""}]]'
|
||
|
||
# 清空整片区域请用 range clear
|
||
dws sheet range clear --node NODE_ID --sheet-id SHEET_ID --range "A1:B3"`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "node", "sheet-id", "range", "values"); err != nil {
|
||
return err
|
||
}
|
||
valuesStr := mustGetFlag(cmd, "values")
|
||
var values [][]any
|
||
if err := json.Unmarshal([]byte(valuesStr), &values); err != nil {
|
||
return fmt.Errorf("--values JSON 解析失败: %w", err)
|
||
}
|
||
for i, row := range values {
|
||
for j, cell := range row {
|
||
path := fmt.Sprintf("--values[%d][%d]", i, j)
|
||
if cell == nil {
|
||
return fmt.Errorf("%s: 不支持 null,每个单元格必须是 {type:text,...}、{type:richText,...}、{hyperlink:...} 对象,或 {} 表示保留原值;运行 'dws sheet range update --help' 查看完整形态与示例", path)
|
||
}
|
||
cellMap, ok := cell.(map[string]any)
|
||
if !ok {
|
||
return fmt.Errorf("%s: 不支持原始值 %v,每个单元格必须是 object(如 {\"type\":\"text\",\"text\":\"...\"});运行 'dws sheet range update --help' 查看完整形态与示例", path, cell)
|
||
}
|
||
if len(cellMap) == 0 {
|
||
continue
|
||
}
|
||
if err := validateComplexValueCell(cellMap, path); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return callMCPTool("set_cell_range", map[string]any{
|
||
"nodeId": mustGetFlag(cmd, "node"),
|
||
"sheetId": mustGetFlag(cmd, "sheet-id"),
|
||
"rangeAddress": mustGetFlag(cmd, "range"),
|
||
"cells": values,
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(rangeUpdateCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "sheet",
|
||
Name: "range_update",
|
||
CanonicalPath: "sheet.range_update",
|
||
CLIPath: "sheet range update",
|
||
PrimaryCLIPath: "sheet range update",
|
||
},
|
||
Description: "更新指定区域单元格(值/公式/超链接等 object 协议);少量或需富格式时使用。",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "sheet", RPCName: "update_range"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "更新指定区域单元格(值/公式/超链接等 object 协议);少量或需富格式时使用。",
|
||
UseWhen: []string{"需要写入少量单元格、公式对象、超链接或富格式,且能提供 sheetId 与精确 range 时"},
|
||
AvoidWhen: []string{"CSV 形式的大批量值或公式写入优先 sheet csv-put;末尾追加行用 append;全局替换文本用 replace;只改样式用 range set-style;清整片区域用 range clear"},
|
||
Examples: []string{"dws sheet range update --node <NODE_ID> --sheet-id <SHEET_ID> --range \"A1\" --values '[[{\"type\":\"text\",\"text\":\"张三\"}]]'"},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "node", Property: "nodeId"},
|
||
{Name: "range", Property: "rangeAddress"},
|
||
},
|
||
},
|
||
})
|
||
rangeUpdateCmd.Flags().String("node", "", "表格文档 ID (必填)")
|
||
rangeUpdateCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
|
||
rangeUpdateCmd.Flags().String("range", "", "目标单元格区域地址,如 A1:B3 (必填)")
|
||
rangeUpdateCmd.Flags().String("values", "", "单元格内容,二维 JSON 数组 (必填);每个元素必须是 object:{type:text,text:...}、{type:richText,texts:[...]}、{dataValidation:...}、{cellStyles:...}、{hyperlink:...} 或 {}(详见 --help 长描述)")
|
||
|
||
rangeClearCmd := &cobra.Command{
|
||
Use: "clear",
|
||
Short: "清除工作表指定区域",
|
||
Long: `清除钉钉电子表格中指定工作表指定范围的单元格内容、格式或全部。
|
||
|
||
清除类型 (--type):
|
||
content 仅清除单元格的值,保留格式(默认)
|
||
format 仅清除格式,保留值
|
||
all 清除值和格式
|
||
|
||
范围格式使用 A1 表示法:
|
||
--range A1:B3 清除 A1 到 B3 的矩形区域
|
||
--range A1 清除单个单元格
|
||
--range A:C 清除整列范围
|
||
|
||
该操作会删除内容或格式;必须先获得用户确认,再追加 --yes 执行。`,
|
||
Example: ` dws sheet range clear --node NODE_ID --sheet-id SHEET_ID --range "A1:B3" --yes
|
||
dws sheet range clear --node NODE_ID --sheet-id SHEET_ID --range "A1:B3" --type format --yes
|
||
dws sheet range clear --node NODE_ID --sheet-id SHEET_ID --range "A1:B3" --type all --yes`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "node", "sheet-id", "range"); err != nil {
|
||
return err
|
||
}
|
||
toolArgs := map[string]any{
|
||
"nodeId": mustGetFlag(cmd, "node"),
|
||
"sheetId": mustGetFlag(cmd, "sheet-id"),
|
||
"range": mustGetFlag(cmd, "range"),
|
||
}
|
||
if v, _ := cmd.Flags().GetString("type"); v != "" {
|
||
toolArgs["type"] = v
|
||
}
|
||
return callMCPTool("clear_range", toolArgs)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(rangeClearCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "sheet",
|
||
Name: "clear_range",
|
||
CanonicalPath: "sheet.clear_range",
|
||
CLIPath: "sheet range clear",
|
||
PrimaryCLIPath: "sheet range clear",
|
||
},
|
||
Description: "清除指定范围的内容、格式或全部(需确认后加 --yes)。",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "sheet", RPCName: "clear_range"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "清除指定范围的内容、格式或全部(需确认后加 --yes)。",
|
||
UseWhen: []string{"需要清空一片区域且保留行列占位时"},
|
||
AvoidWhen: []string{"要物理删除行列用 delete-dimension;删整张工作表用 delete-sheet;单格写空可用 range update 但整片清除应优先本命令"},
|
||
Examples: []string{"dws sheet range clear --node <NODE_ID> --sheet-id <SHEET_ID> --range \"A1:B3\" --type content"},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "node", Property: "nodeId"},
|
||
},
|
||
},
|
||
})
|
||
rangeClearCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
|
||
rangeClearCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
|
||
rangeClearCmd.Flags().String("range", "", "清除范围,A1 表示法 (必填,如 A1:B3)")
|
||
rangeClearCmd.Flags().String("type", "", "清除类型: content(仅值,默认) / format(仅格式) / all(全部)")
|
||
|
||
rangeSortCmd := &cobra.Command{
|
||
Use: "sort",
|
||
Short: "对工作表指定区域排序",
|
||
Long: `对钉钉电子表格中指定工作表指定范围的数据进行排序。
|
||
|
||
支持多级排序(按 --sort-keys 数组顺序优先级递减)。
|
||
column 使用字母列名(如 "A"、"B"、"AA"),表示排序的目标列。
|
||
|
||
注意:排序前必须先 range read 排序范围的前 3-5 行,对比首行与后续行模式判断表头:
|
||
- 首行全文本 + 后续行含数字/日期 → 加 --has-header
|
||
- 首行与后续行模式一致 → 不加
|
||
- 首行语义像列标题且与后续行明显不同 → 加 --has-header
|
||
表头误排入数据不可撤销,禁止不读就排。`,
|
||
Example: ` dws sheet range sort --node NODE_ID --sheet-id SHEET_ID --range "A1:D10" \
|
||
--sort-keys '[{"column":"A","ascending":true}]'
|
||
dws sheet range sort --node NODE_ID --sheet-id SHEET_ID --range "A1:D10" \
|
||
--sort-keys '[{"column":"A","ascending":true},{"column":"C","ascending":false}]' --has-header`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "node", "sheet-id", "range", "sort-keys"); err != nil {
|
||
return err
|
||
}
|
||
toolArgs := map[string]any{
|
||
"nodeId": mustGetFlag(cmd, "node"),
|
||
"sheetId": mustGetFlag(cmd, "sheet-id"),
|
||
"range": mustGetFlag(cmd, "range"),
|
||
}
|
||
sortKeysStr, _ := cmd.Flags().GetString("sort-keys")
|
||
var sortKeys []any
|
||
if err := json.Unmarshal([]byte(sortKeysStr), &sortKeys); err != nil {
|
||
return fmt.Errorf("--sort-keys JSON 解析失败: %w", err)
|
||
}
|
||
toolArgs["sortKeys"] = sortKeys
|
||
if v, _ := cmd.Flags().GetBool("has-header"); v {
|
||
toolArgs["hasHeader"] = true
|
||
}
|
||
return callMCPTool("sort_range", toolArgs)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(rangeSortCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "sheet",
|
||
Name: "sort_range",
|
||
CanonicalPath: "sheet.sort_range",
|
||
CLIPath: "sheet range sort",
|
||
PrimaryCLIPath: "sheet range sort",
|
||
},
|
||
Description: "对指定区域排序(改变物理行序)。",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "sheet", RPCName: "sort_range"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "对指定区域排序(改变物理行序)。",
|
||
UseWhen: []string{"需要按某列对一块矩形区域排序,且不依赖全局筛选时"},
|
||
AvoidWhen: []string{"已有全局筛选时按筛选范围排序用 filter sort;不要用读出再写回模拟排序"},
|
||
Examples: []string{"dws sheet range sort --node NODE_ID --sheet-id SHEET_ID --range A1:D10 --sort-keys '[{\"column\":\"A\",\"ascending\":true}]'"},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "node", Property: "nodeId"},
|
||
},
|
||
},
|
||
})
|
||
rangeSortCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
|
||
rangeSortCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
|
||
rangeSortCmd.Flags().String("range", "", "排序范围,A1 表示法 (必填,如 A1:D10)")
|
||
rangeSortCmd.Flags().String("sort-keys", "", `排序规则 JSON 数组 (必填,如 [{"column":"A","ascending":true}])`)
|
||
rangeSortCmd.Flags().Bool("has-header", false, "首行是否为表头(不参与排序)")
|
||
|
||
rangeFillCmd := &cobra.Command{
|
||
Use: "fill",
|
||
Short: "自动填充工作表指定区域",
|
||
Long: `基于源数据范围自动填充到目标范围。
|
||
|
||
目标范围须与源范围在行或列维度对齐(不支持对角填充)。
|
||
|
||
填充类型 (--fill-type):
|
||
不传 自动检测(根据源数据智能判断:数值序列递增、日期递增、文本复制等)
|
||
copy 复制内容和格式
|
||
onlystyle 仅格式
|
||
withoutstyle 仅值无格式`,
|
||
Example: ` dws sheet range fill --node NODE_ID --sheet-id SHEET_ID \
|
||
--source-range "A1:A5" --target-range "A6:A20"
|
||
dws sheet range fill --node NODE_ID --sheet-id SHEET_ID \
|
||
--source-range "A1:A5" --target-range "A6:A20" --fill-type copy`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "node", "sheet-id", "source-range", "target-range"); err != nil {
|
||
return err
|
||
}
|
||
toolArgs := map[string]any{
|
||
"nodeId": mustGetFlag(cmd, "node"),
|
||
"sheetId": mustGetFlag(cmd, "sheet-id"),
|
||
"sourceRange": mustGetFlag(cmd, "source-range"),
|
||
"destinationRange": mustGetFlag(cmd, "target-range"),
|
||
}
|
||
if v, _ := cmd.Flags().GetString("fill-type"); v != "" {
|
||
toolArgs["fillType"] = v
|
||
}
|
||
return callMCPTool("fill_range", toolArgs)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(rangeFillCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "sheet",
|
||
Name: "fill_range",
|
||
CanonicalPath: "sheet.fill_range",
|
||
CLIPath: "sheet range fill",
|
||
PrimaryCLIPath: "sheet range fill",
|
||
},
|
||
Description: "按源区域对目标区域做自动填充(复制或序列)。",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "sheet", RPCName: "fill_range"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "按源区域对目标区域做自动填充(复制或序列)。",
|
||
UseWhen: []string{"需要像拖拽填充一样把源范围扩展到目标范围时"},
|
||
AvoidWhen: []string{"直接写死值用 range update/csv-put;区域复制到另一位置用 range copy-to"},
|
||
Examples: []string{"dws sheet range fill --node <NODE_ID> --sheet-id <SHEET_ID> --source-range \"A1:A5\" --target-range \"A6:A20\""},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "node", Property: "nodeId"},
|
||
{Name: "target-range", Property: "destinationRange"},
|
||
},
|
||
},
|
||
})
|
||
rangeFillCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
|
||
rangeFillCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)")
|
||
rangeFillCmd.Flags().String("source-range", "", "源数据范围,A1 表示法 (必填,如 A1:A5)")
|
||
rangeFillCmd.Flags().String("target-range", "", "目标填充范围,A1 表示法 (必填,如 A6:A20)")
|
||
rangeFillCmd.Flags().String("fill-type", "", "填充类型: 不传则自动检测 / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值)")
|
||
|
||
rangeCopyCmd := &cobra.Command{
|
||
Use: "copy-to",
|
||
Short: "复制工作表指定区域到目标位置",
|
||
Long: `将源范围的数据复制到目标位置。支持跨工作表复制。
|
||
|
||
跨工作表复制的两种方式:
|
||
1. --target-sheet-id "Sheet2" 显式指定目标工作表
|
||
2. --target-range "Sheet2!A1" 在目标范围中携带工作表前缀
|
||
|
||
限制:
|
||
- 同一工作表内源和目标范围不能重叠`,
|
||
Example: ` dws sheet range copy-to --node NODE_ID --sheet-id SHEET_ID \
|
||
--source-range "A1:C5" --target-range "D1"
|
||
dws sheet range copy-to --node NODE_ID --sheet-id SHEET_ID \
|
||
--source-range "A1:C5" --target-range "A1" --target-sheet-id Sheet2
|
||
dws sheet range copy-to --node NODE_ID --sheet-id SHEET_ID \
|
||
--source-range "A1:C5" --target-range "Sheet2!A1"`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "node", "sheet-id", "source-range", "target-range"); err != nil {
|
||
return err
|
||
}
|
||
toolArgs := map[string]any{
|
||
"nodeId": mustGetFlag(cmd, "node"),
|
||
"sheetId": mustGetFlag(cmd, "sheet-id"),
|
||
"sourceRange": mustGetFlag(cmd, "source-range"),
|
||
"destinationRange": mustGetFlag(cmd, "target-range"),
|
||
}
|
||
if v, _ := cmd.Flags().GetString("target-sheet-id"); v != "" {
|
||
toolArgs["targetSheetId"] = v
|
||
}
|
||
if v, _ := cmd.Flags().GetString("paste-type"); v != "" {
|
||
toolArgs["pasteType"] = v
|
||
}
|
||
return callMCPTool("copy_range", toolArgs)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(rangeCopyCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "sheet",
|
||
Name: "range_copy_to",
|
||
CanonicalPath: "sheet.range_copy_to",
|
||
CLIPath: "sheet range copy-to",
|
||
PrimaryCLIPath: "sheet range copy-to",
|
||
},
|
||
Description: "将源范围复制到目标位置。",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "sheet", RPCName: "copy_range"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "将源范围复制到目标位置。",
|
||
UseWhen: []string{"需要把一块区域原样复制到另一位置(可跨工作表)时"},
|
||
AvoidWhen: []string{"移动而不是复制用 range move-to;自动填充序列用 range fill"},
|
||
Examples: []string{"dws sheet range copy-to --node NODE_ID --sheet-id SHEET_ID --source-range \"A1:C5\" --target-range \"E1\""},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "node", Property: "nodeId"},
|
||
{Name: "target-range", Property: "destinationRange"},
|
||
},
|
||
},
|
||
})
|
||
rangeCopyCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
|
||
rangeCopyCmd.Flags().String("sheet-id", "", "源工作表 ID 或名称 (必填)")
|
||
rangeCopyCmd.Flags().String("source-range", "", "源范围,A1 表示法 (必填,如 A1:C5)")
|
||
rangeCopyCmd.Flags().String("target-range", "", "目标位置,A1 表示法 (必填,如 D1;支持 Sheet2!A1 表示法指定目标工作表)")
|
||
rangeCopyCmd.Flags().String("target-sheet-id", "", "目标工作表 ID 或名称(可选,不传则复制到同一工作表)")
|
||
rangeCopyCmd.Flags().String("paste-type", "", "粘贴类型: values(仅值) / formulas(仅公式) / formats(仅格式) / all(全部,默认)")
|
||
|
||
rangeMoveCmd := &cobra.Command{
|
||
Use: "move-to",
|
||
Short: "移动工作表指定区域到目标位置",
|
||
Long: `将源范围的数据移动到目标位置,源区域将被清空。支持跨工作表移动。
|
||
|
||
跨工作表移动的两种方式:
|
||
1. --target-sheet-id "Sheet2" 显式指定目标工作表
|
||
2. --target-range "Sheet2!A1" 在目标范围中携带工作表前缀
|
||
|
||
限制:
|
||
- 同一工作表内源和目标范围不能重叠`,
|
||
Example: ` dws sheet range move-to --node NODE_ID --sheet-id SHEET_ID \
|
||
--source-range "A1:C5" --target-range "D1" --yes
|
||
dws sheet range move-to --node NODE_ID --sheet-id SHEET_ID \
|
||
--source-range "A1:C5" --target-range "A1" --target-sheet-id Sheet2 --yes
|
||
dws sheet range move-to --node NODE_ID --sheet-id SHEET_ID \
|
||
--source-range "A1:C5" --target-range "Sheet2!A1" --yes`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if err := validateRequiredFlags(cmd, "node", "sheet-id", "source-range", "target-range"); err != nil {
|
||
return err
|
||
}
|
||
toolArgs := map[string]any{
|
||
"nodeId": mustGetFlag(cmd, "node"),
|
||
"sheetId": mustGetFlag(cmd, "sheet-id"),
|
||
"sourceRange": mustGetFlag(cmd, "source-range"),
|
||
"destinationRange": mustGetFlag(cmd, "target-range"),
|
||
}
|
||
if v, _ := cmd.Flags().GetString("target-sheet-id"); v != "" {
|
||
toolArgs["targetSheetId"] = v
|
||
}
|
||
return callMCPTool("move_range", toolArgs)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(rangeMoveCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "sheet",
|
||
Name: "range_move_to",
|
||
CanonicalPath: "sheet.range_move_to",
|
||
CLIPath: "sheet range move-to",
|
||
PrimaryCLIPath: "sheet range move-to",
|
||
},
|
||
Description: "将源范围移动到目标位置(需确认后加 --yes)。",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "sheet", RPCName: "move_range"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "将源范围移动到目标位置(需确认后加 --yes)。",
|
||
UseWhen: []string{"需要把区域从源位置挪到目标位置且源处应腾空时"},
|
||
AvoidWhen: []string{"只需复制保留源用 range copy-to;移动整行/整列用 move-dimension"},
|
||
Examples: []string{"dws sheet range move-to --node NODE_ID --sheet-id SHEET_ID --source-range \"A1:C5\" --target-range \"E1\""},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "node", Property: "nodeId"},
|
||
{Name: "target-range", Property: "destinationRange"},
|
||
},
|
||
},
|
||
})
|
||
rangeMoveCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)")
|
||
rangeMoveCmd.Flags().String("sheet-id", "", "源工作表 ID 或名称 (必填)")
|
||
rangeMoveCmd.Flags().String("source-range", "", "源范围,A1 表示法 (必填,如 A1:C5)")
|
||
rangeMoveCmd.Flags().String("target-range", "", "目标位置,A1 表示法 (必填,如 D1;支持 Sheet2!A1 表示法指定目标工作表)")
|
||
rangeMoveCmd.Flags().String("target-sheet-id", "", "目标工作表 ID 或名称(可选,不传则移动到同一工作表)")
|
||
|
||
rangeCmd.AddCommand(rangeReadCmd, rangeUpdateCmd, rangeClearCmd, rangeSortCmd, rangeFillCmd, rangeCopyCmd, rangeMoveCmd)
|
||
return rangeCmd
|
||
}
|