Files

246 lines
11 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 sheet declares high-fidelity shortcuts for the DingTalk sheet MCP
// product. Tool names and parameter keys mirror the helper commands under
// internal/helpers/sheet_*.go verbatim.
package sheet
import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// ── workbook & worksheet ──────────────────────────────────────────────
// Create creates a new DingTalk online spreadsheet document.
// ListSheets lists all worksheets in a spreadsheet document.
var ListSheets = shortcut.Shortcut{
Service: "sheet",
Command: "+list-sheets",
Product: "sheet",
Description: "获取表格文档中全部工作表列表",
Intent: "当你拿到一个表格文档、想先了解它里面有哪些工作表(sheet)以及各自的 sheetId 时使用,通常作为读写具体数据前的第一步;传入表格文档 ID 或 URL,返回工作表清单。",
Risk: shortcut.RiskRead,
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: corecmd.ContractDecl{
Identity: contract.ToolIdentitySpec{
ProductID: "sheet",
Name: "shortcut_list_sheets",
CanonicalPath: "sheet.shortcut_list_sheets",
CLIPath: "sheet +list-sheets",
PrimaryCLIPath: "sheet +list-sheets",
},
Description: "获取表格文档中全部工作表列表",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
},
Selection: contract.SelectionSpec{
AgentSummary: "获取表格文档中全部工作表列表",
UseWhen: []string{"当你拿到一个表格文档、想先了解它里面有哪些工作表(sheet)以及各自的 sheetId 时使用,通常作为读写具体数据前的第一步;传入表格文档 ID 或 URL,返回工作表清单。"},
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws sheet +list-sheets --node NODE_ID"},
},
},
Flags: []shortcut.Flag{
{Name: "node", Type: shortcut.FlagString, Desc: "表格文档 ID 或 URL", Required: true},
},
Tips: []string{`dws sheet +list-sheets --node NODE_ID`},
Execute: func(rt *shortcut.RuntimeContext) error {
data, err := rt.CallMCPData("sheet", "get_all_sheets", map[string]any{"nodeId": rt.Str("node")})
if err != nil {
return err
}
sheets := listSheetsProject(data)
return rt.Output(map[string]any{"count": len(sheets), "sheets": sheets})
},
}
// listSheetsProject reshapes the raw get_all_sheets response into a clean
// worksheet list (sheetId/title/index/visibility/rowCount/columnCount) — the
// the clean output projection applied to every list command. Both
// the list container and the per-item field names are probed defensively across
// candidate keys, so an empty/unknown shape yields an empty list rather than a
// crash or fabricated data.
func listSheetsProject(data map[string]any) []map[string]any {
raw := listSheetsResolveList(data)
out := make([]map[string]any, 0, len(raw))
for _, item := range raw {
m, ok := item.(map[string]any)
if !ok {
continue
}
row := map[string]any{}
if v, ok := listSheetsFirst(m, "sheetId", "sheet_id", "id"); ok {
row["sheetId"] = v
}
if v, ok := listSheetsFirst(m, "title", "name", "sheetName", "sheet_name"); ok {
row["title"] = v
}
if v, ok := listSheetsFirst(m, "index", "sheetIndex"); ok {
row["index"] = v
}
if v, ok := listSheetsFirst(m, "visibility", "hidden", "visible"); ok {
row["visibility"] = v
}
if v, ok := listSheetsFirst(m, "rowCount", "row_count", "rows"); ok {
row["rowCount"] = v
}
if v, ok := listSheetsFirst(m, "columnCount", "column_count", "columns", "colCount"); ok {
row["columnCount"] = v
}
if len(row) > 0 {
out = append(out, row)
}
}
return out
}
// listSheetsResolveList locates the worksheet array inside the response,
// tolerating a bare top-level array container or nesting one level deeper under
// a common envelope key.
func listSheetsResolveList(data map[string]any) []any {
if data == nil {
return []any{}
}
for _, key := range []string{"sheets", "result", "data", "list", "items", "worksheets"} {
v, ok := data[key]
if !ok {
continue
}
if arr, ok := v.([]any); ok {
return arr
}
if inner, ok := v.(map[string]any); ok {
for _, ik := range []string{"sheets", "list", "items", "worksheets", "result", "data"} {
if arr, ok := inner[ik].([]any); ok {
return arr
}
}
}
}
return []any{}
}
// listSheetsFirst returns the first present candidate key's value.
func listSheetsFirst(m map[string]any, keys ...string) (any, bool) {
for _, k := range keys {
if v, ok := m[k]; ok {
return v, true
}
}
return nil, false
}
// SheetInfo returns the detail of a single worksheet.
// SheetCreate adds a new worksheet to a spreadsheet.
// SheetUpdate updates worksheet properties (name/position/hidden/freeze/tab color).
// SheetCopy duplicates a worksheet within the same spreadsheet.
// SheetDelete removes a worksheet (irreversible).
// ── data read/write ───────────────────────────────────────────────────
// Read reads structured per-cell data from a worksheet range.
var Read = shortcut.Shortcut{
Service: "sheet",
Command: "+read",
Product: "sheet",
Description: "读取工作表指定范围的结构化单元格数据",
Intent: "当你需要按单元格逐格获取数据(含类型、公式或格式化值等结构化信息)以便程序处理时使用;传入表格与可选范围(A1 表示法,不传则全部),可指定取格式化值/原始值/公式,返回结构化单元格数组。若只想要纯文本可改用 +csv-get。",
Risk: shortcut.RiskRead,
Safety: contract.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Contract: corecmd.ContractDecl{
Identity: contract.ToolIdentitySpec{
ProductID: "sheet",
Name: "shortcut_read",
CanonicalPath: "sheet.shortcut_read",
CLIPath: "sheet +read",
PrimaryCLIPath: "sheet +read",
},
Description: "读取工作表指定范围的结构化单元格数据",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
},
Selection: contract.SelectionSpec{
AgentSummary: "读取工作表指定范围的结构化单元格数据",
UseWhen: []string{"当你需要按单元格逐格获取数据(含类型、公式或格式化值等结构化信息)以便程序处理时使用;传入表格与可选范围(A1 表示法,不传则全部),可指定取格式化值/原始值/公式,返回结构化单元格数组。若只想要纯文本可改用 +csv-get。"},
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{"dws sheet +read --node NODE_ID --sheet-id SHEET_ID --range \"A1:D10\""},
},
},
Flags: []shortcut.Flag{
{Name: "node", Type: shortcut.FlagString, Desc: "表格文档 ID 或 URL", Required: true},
{Name: "sheet-id", Type: shortcut.FlagString, Desc: "工作表 ID 或名称 (不传则第一个工作表)"},
{Name: "range", Type: shortcut.FlagString, Desc: "读取范围,A1 表示法 (不传则全部数据)"},
{Name: "value-render-option", Type: shortcut.FlagString, Desc: "取值模式", Enum: []string{"formatted_value", "raw_value", "formula"}},
},
Tips: []string{`dws sheet +read --node NODE_ID --sheet-id SHEET_ID --range "A1:D10"`},
Execute: func(rt *shortcut.RuntimeContext) error {
params := map[string]any{"nodeId": rt.Str("node")}
if rt.Changed("sheet-id") {
params["sheetId"] = rt.Str("sheet-id")
}
if rt.Changed("range") {
params["range"] = rt.Str("range")
}
if rt.Changed("value-render-option") {
params["valueRenderOption"] = rt.Str("value-render-option")
}
return rt.CallMCP("get_cell_infos", params)
},
}
// Write updates a worksheet range with a 2D JSON array of cell objects.
// Append appends rows to the end of a worksheet.
// CSVGet reads worksheet data as RFC 4180 CSV.
// CSVPut writes CSV text into a worksheet at a start cell (values only).
// CellsSearch finds cells matching a text query.
// CellsReplace finds and replaces text across a worksheet.
// CellsClear clears content/format of a worksheet range.
// ── range operations ──────────────────────────────────────────────────
// RangeSort sorts a worksheet range by one or more keys.
// RangeFill auto-fills a target range from a source range.
// RangeCopy copies a source range to a target location (supports cross-sheet).
// RangeMove moves a source range to a target location (source is cleared).
// ── dimensions & merge ────────────────────────────────────────────────
// InsertDimension inserts empty rows or columns before a position.
// AddDimension appends empty rows or columns at the end of a worksheet.
// MoveDimension moves rows or columns to a destination position.
// UpdateDimension updates hidden state and/or row height / column width.
// DeleteDimension deletes rows or columns from a position (irreversible).
// MergeCells merges a range of cells.
// UnmergeCells unmerges cells within a range.
// ── dropdown lists ────────────────────────────────────────────────────
// SetDropdown sets a dropdown list on a cell range.
// GetDropdown reads dropdown list configuration for a range.
// DeleteDropdown removes dropdown list configuration from a range.
func init() {
shortcut.Register(
ListSheets,
Read,
)
}