Merge pull request #1083 from zhengzhijiej-tech/fix/dropdown-get-sheet-id-prefix

fix(sheets): +dropdown-get accepts --sheet-id/--sheet-name + bare --range
This commit is contained in:
zhengzhijiej-tech
2026-05-25 21:28:30 +08:00
committed by GitHub
5 changed files with 73 additions and 34 deletions

View File

@@ -1180,7 +1180,7 @@
"kind": "own",
"type": "string",
"required": "required",
"desc": "A1 range, e.g. `Sheet1!A1:F10`"
"desc": "A1 range, e.g. `A1:F10` (no sheet prefix — use `--sheet-id` / `--sheet-name` to select the sheet)"
},
{
"name": "include",
@@ -1238,12 +1238,26 @@
"required": "xor",
"desc": "Spreadsheet token (XOR with `--url`)"
},
{
"name": "sheet-id",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Sheet reference_id (XOR with `--sheet-name`)"
},
{
"name": "sheet-name",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Sheet name (XOR with `--sheet-id`)"
},
{
"name": "range",
"kind": "own",
"type": "string",
"required": "required",
"desc": "Target range (A1 notation; must include the sheet prefix, e.g. `sheet1!A2:A100`)"
"desc": "Target range in A1 notation, e.g. `A2:A100` (no sheet prefix — use `--sheet-id` / `--sheet-name` to select the sheet)"
},
{
"name": "dry-run",
@@ -1290,7 +1304,7 @@
"kind": "own",
"type": "string",
"required": "required",
"desc": "A1 range, e.g. `Sheet1!A1:F30`"
"desc": "A1 range, e.g. `A1:F30` (no sheet prefix — use `--sheet-id` / `--sheet-name` to select the sheet)"
},
{
"name": "value-render-option",
@@ -1951,7 +1965,7 @@
"kind": "own",
"type": "string",
"required": "required",
"desc": "Top-left A1 anchor (e.g. `Sheet1!A1`); the bottom-right is inferred from CSV row/column counts",
"desc": "Top-left A1 anchor (e.g. `A1`, `B5`; no sheet prefix — use `--sheet-id` / `--sheet-name` to select the sheet); must be a single cell, range notation not accepted; the bottom-right is inferred from CSV row/column counts",
"default": "A1"
},
{

View File

@@ -215,16 +215,16 @@ func stripRowPrefixFromCsvOutput(out interface{}) interface{} {
}
// DropdownGet wraps get_cell_ranges scoped to data_validation: read the
// dropdown configuration on a range. The CLI accepts the range in the
// sheet-prefixed form (e.g. "sheet1!A2:A100") for convenience; the
// prefix is split client-side into sheet_name + bare A1 because the
// get_cell_ranges tool wants sheet selector and ranges as separate
// fields (ranges with the "sheet!" prefix gets the empty-sheet_id
// rejection from the server).
// dropdown configuration on a range. Aligned with its sibling +cells-get
// sheet selection is via --sheet-id / --sheet-name (XOR), and --range
// is a bare A1 reference. The earlier "must include a sheet prefix"
// shape was the odd one out among the get_cell_ranges wrappers and made
// callers treat the prefix as either name or id; folding it into the
// canonical --sheet-id selector removes that ambiguity.
var DropdownGet = common.Shortcut{
Service: "sheets",
Command: "+dropdown-get",
Description: "Read the dropdown / data-validation configuration on a sheet-prefixed range.",
Description: "Read the dropdown / data-validation configuration on a range.",
Risk: "read",
Scopes: []string{"sheets:spreadsheet:read"},
AuthTypes: []string{"user", "bot"},
@@ -234,24 +234,29 @@ var DropdownGet = common.Shortcut{
if _, err := resolveSpreadsheetToken(runtime); err != nil {
return err
}
if _, _, err := resolveSheetSelector(runtime); err != nil {
return err
}
if strings.TrimSpace(runtime.Str("range")) == "" {
return common.FlagErrorf("--range is required")
}
if !strings.Contains(runtime.Str("range"), "!") {
return common.FlagErrorf("--range must include a sheet prefix (e.g. sheet1!A2:A100)")
}
return nil
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
return invokeToolDryRun(token, ToolKindRead, "get_cell_ranges", dropdownGetInput(runtime, token))
sheetID, sheetName, _ := resolveSheetSelector(runtime)
return invokeToolDryRun(token, ToolKindRead, "get_cell_ranges", dropdownGetInput(runtime, token, sheetID, sheetName))
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetToken(runtime)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindRead, "get_cell_ranges", dropdownGetInput(runtime, token))
sheetID, sheetName, err := resolveSheetSelector(runtime)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindRead, "get_cell_ranges", dropdownGetInput(runtime, token, sheetID, sheetName))
if err != nil {
return err
}
@@ -260,16 +265,13 @@ var DropdownGet = common.Shortcut{
},
}
func dropdownGetInput(runtime *common.RuntimeContext, token string) map[string]interface{} {
// Validate already enforced the "Sheet!range" prefix, so the
// split error path can't be reached here in practice.
sheetName, bareRange, _ := splitSheetPrefixedRange(strings.TrimSpace(runtime.Str("range")))
func dropdownGetInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} {
input := map[string]interface{}{
"excel_id": token,
"ranges": []string{bareRange},
"ranges": []string{strings.TrimSpace(runtime.Str("range"))},
"include_styles": false,
"value_render_option": "formatted_value",
}
sheetSelectorForToolInput(input, "", sheetName)
sheetSelectorForToolInput(input, sheetID, sheetName)
return input
}

View File

@@ -48,14 +48,30 @@ func TestReadDataShortcuts_DryRun(t *testing.T) {
},
},
{
name: "+dropdown-get range with sheet prefix only",
// Canonical form: --sheet-id + bare --range. Aligned with
// +cells-get / +csv-get; before the e2e BUG-019 fix this
// shortcut was the odd one out (range-prefix required).
name: "+dropdown-get with --sheet-id",
sc: DropdownGet,
args: []string{"--url", testURL, "--range", "sheet1!A2:A100"},
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "C2:C6"},
toolName: "get_cell_ranges",
wantInput: map[string]interface{}{
"excel_id": testToken,
"sheet_name": "sheet1",
"ranges": []interface{}{"A2:A100"},
"sheet_id": testSheetID,
"ranges": []interface{}{"C2:C6"},
"include_styles": false,
"value_render_option": "formatted_value",
},
},
{
name: "+dropdown-get with --sheet-name",
sc: DropdownGet,
args: []string{"--url", testURL, "--sheet-name", "Sheet1", "--range", "C2:C6"},
toolName: "get_cell_ranges",
wantInput: map[string]interface{}{
"excel_id": testToken,
"sheet_name": "Sheet1",
"ranges": []interface{}{"C2:C6"},
"include_styles": false,
"value_render_option": "formatted_value",
},
@@ -72,7 +88,12 @@ func TestReadDataShortcuts_DryRun(t *testing.T) {
}
}
func TestDropdownGet_RequiresSheetPrefix(t *testing.T) {
// TestDropdownGet_RequiresSheetSelector locks the +cells-get-style
// selector contract: at least one of --sheet-id / --sheet-name must be
// supplied. Before BUG-019 fix this shortcut required a "Sheet!A1"
// prefix inside --range instead; the canonical selector pair is what
// every other get_cell_ranges wrapper uses.
func TestDropdownGet_RequiresSheetSelector(t *testing.T) {
t.Parallel()
stdout, stderr, err := runShortcutCapturingErr(t, DropdownGet, []string{
"--url", testURL, "--range", "A2:A100", "--dry-run",
@@ -80,8 +101,9 @@ func TestDropdownGet_RequiresSheetPrefix(t *testing.T) {
if err == nil {
t.Fatalf("expected validation error; stdout=%s stderr=%s", stdout, stderr)
}
if !strings.Contains(stdout+stderr+err.Error(), "must include a sheet prefix") {
t.Errorf("expected sheet-prefix guard; got=%s|%s|%v", stdout, stderr, err)
combined := stdout + stderr + err.Error()
if !strings.Contains(combined, "sheet-id") && !strings.Contains(combined, "sheet-name") {
t.Errorf("expected --sheet-id/--sheet-name guard; got=%s|%s|%v", stdout, stderr, err)
}
}
@@ -96,6 +118,7 @@ func TestReadData_RequiresRange(t *testing.T) {
}{
{"+cells-get", CellsGet},
{"+csv-get", CsvGet},
{"+dropdown-get", DropdownGet},
}
for _, c := range cases {
c := c

View File

@@ -90,18 +90,18 @@ _公共四件套 · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--range` | string | required | A1 范围,如 `Sheet1!A1:F10` |
| `--range` | string | required | A1 范围,如 `A1:F10`(不带 sheet 前缀;用 `--sheet-id` / `--sheet-name` 指定 sheet |
| `--include` | string_slice | optional | 要返回的信息类别,逗号分隔多个(可选值:`value` / `formula` / `style` / `comment` / `data_validation` |
| `--max-chars` | int | optional | 防爆,默认 200000隐藏 flag不在 `--help` 列出,但可正常传入) |
| `--skip-hidden` | bool | optional | 跳过隐藏行列,默认 `false` |
### `+dropdown-get`
_公共URL/token无 sheet 定位) · 系统:`--dry-run`_
_公共四件套 · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--range` | string | required | 目标范围A1 格式,必须带 sheet 前缀,如 `sheet1!A2:A100` |
| `--range` | string | required | A1 范围,如 `A2:A100`(不带 sheet 前缀;用 `--sheet-id` / `--sheet-name` 指定 sheet |
### `+csv-get`
@@ -109,7 +109,7 @@ _公共四件套 · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--range` | string | required | A1 范围,如 `Sheet1!A1:F30` |
| `--range` | string | required | A1 范围,如 `A1:F30`(不带 sheet 前缀;用 `--sheet-id` / `--sheet-name` 指定 sheet |
| `--value-render-option` | string | optional | 单元格取值模式(可选值:`formatted_value` / `raw_value` / `formula`)(默认 `formatted_value` |
| `--max-chars` | int | optional | 防爆,默认 200000隐藏 flag不在 `--help` 列出,但可正常传入) |
| `--include-row-prefix` | bool | optional | 是否在每行前加 `[row=N]` 前缀,默认 `true` |

View File

@@ -286,7 +286,7 @@ _公共四件套 · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--start-cell` | string | required | 目标区域起点 A1`Sheet1!A1`;终点按 CSV 实际行列数自动推断 |
| `--start-cell` | string | required | 目标区域起点 A1`A1``B5`,不带 sheet 前缀;用 `--sheet-id` / `--sheet-name` 指定 sheet必须是单个单元格不接受范围写法;终点按 CSV 实际行列数自动推断 |
| `--csv` | string + File + Stdin非 JSON 文本) | required | RFC 4180 CSV 文本;只写纯值,不带公式/样式/批注 |
| `--allow-overwrite` | bool | optional | 允许覆盖(默认 true设为 false 时若目标非空报错 |