From 9eefd681b3f8ddf369a583d0eecf4776e0cb2eb8 Mon Sep 17 00:00:00 2001 From: "zhouyue.z" Date: Fri, 31 Jul 2026 11:07:48 +0800 Subject: [PATCH] fix(base): confirm table URL selections --- shortcuts/base/base_resolve.go | 36 ++++++++++++++++- shortcuts/base/base_resolve_test.go | 39 +++++++++++++++++++ .../base/base_url_resolve_dryrun_test.go | 4 ++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/shortcuts/base/base_resolve.go b/shortcuts/base/base_resolve.go index 31de0dbec6..65264a8835 100644 --- a/shortcuts/base/base_resolve.go +++ b/shortcuts/base/base_resolve.go @@ -32,6 +32,7 @@ var BaseURLResolve = common.Shortcut{ "base:block:read", "base:field:read", "base:record:read", + "base:table:read", "wiki:node:retrieve", }, AuthTypes: authTypes(), @@ -61,9 +62,15 @@ var BaseURLResolve = common.Shortcut{ case "base_url": baseToken := firstPathSegmentAfter(parsed.Path, "/base/") if selectedBlockID := strings.TrimSpace(parsed.Query().Get("table")); selectedBlockID != "" { - return common.NewDryRunAPI(). + dry := common.NewDryRunAPI(). + Desc("2-step: list Base blocks first, then confirm table selection if the block list does not include it") + dry. POST("/open-apis/base/v3/bases/:base_token/blocks/list"). + Desc("[1] List Base blocks and match selected_block_id"). Body(map[string]interface{}{}). + GET("/open-apis/base/v3/bases/:base_token/tables/:selected_block_id"). + Desc("[2] Fallback: confirm selected_block_id as a table when block list is nonmatching") + return dry. Set("base_token", baseToken). Set("selected_block_id", selectedBlockID) } @@ -83,6 +90,8 @@ var BaseURLResolve = common.Shortcut{ dry.POST("/open-apis/base/v3/bases/:base_token/blocks/list"). Desc("[2] List Base blocks and match selected_block_id"). Body(map[string]interface{}{}) + dry.GET("/open-apis/base/v3/bases/:base_token/tables/:selected_block_id"). + Desc("[3] Fallback: confirm selected_block_id as a table when block list is nonmatching") return dry. Set("base_token", ""). Set("selected_block_id", selectedBlockID) @@ -470,6 +479,16 @@ func enrichBaseResolveHint(runtime *common.RuntimeContext, out map[string]interf return } + if table, found := resolveSelectedBaseTable(runtime, baseToken, selectedBlockID); found { + out["block_type"] = "table" + if name := strings.TrimSpace(tableNameFromMap(table)); name != "" { + out["block_name"] = name + } + applyResolvedTableSelection(out, selection) + enrichResolvedTable(runtime, out, baseToken, selectedBlockID) + return + } + out["hint"] = resolveUnknownBlockHint() } @@ -503,6 +522,21 @@ func resolveSelectedBaseBlock(runtime *common.RuntimeContext, baseToken, selecte return resolvedBaseBlock{}, false, nil } +func resolveSelectedBaseTable(runtime *common.RuntimeContext, baseToken, selectedTableID string) (map[string]interface{}, bool) { + table, err := baseV3Call(runtime, "GET", baseV3Path("bases", baseToken, "tables", selectedTableID), nil, nil) + if err != nil { + return nil, false + } + confirmedID := strings.TrimSpace(tableID(table)) + if confirmedID != "" && confirmedID != selectedTableID { + return nil, false + } + if confirmedID == "" { + table["id"] = selectedTableID + } + return table, true +} + func enrichResolvedTable(runtime *common.RuntimeContext, out map[string]interface{}, baseToken, tableID string) { out["table_id"] = tableID fields, total, err := listAllFields(runtime, baseToken, tableID, 0, 100) diff --git a/shortcuts/base/base_resolve_test.go b/shortcuts/base/base_resolve_test.go index c1579e9c2a..0e47e16deb 100644 --- a/shortcuts/base/base_resolve_test.go +++ b/shortcuts/base/base_resolve_test.go @@ -45,6 +45,31 @@ func TestBaseURLResolveBaseURL(t *testing.T) { } }) + t.Run("table selection falls back when block list omits table", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(baseBlockListResolveStub("bas123", + map[string]interface{}{"id": "blk_dashboard", "type": "dashboard", "name": "Sales"}, + )) + reg.Register(tableGetResolveStub("bas123", "tbl123", "Orders")) + reg.Register(fieldListStub("bas123", "tbl123")) + err := runShortcutWithAuthTypes(t, BaseURLResolve, authTypes(), []string{ + "+url-resolve", + "--url", "https://example.larkoffice.com/base/bas123?table=tbl123&view=vew123&record=rec123", + "--as", "user", + }, factory, stdout) + if err != nil { + t.Fatalf("err=%v", err) + } + + data := decodeBaseEnvelope(t, stdout) + if data["block_id"] != "tbl123" || data["selection_source"] != "url_query" || data["block_type"] != "table" || data["table_id"] != "tbl123" || data["view_id"] != "vew123" || data["record_id"] != "rec123" { + t.Fatalf("missing fallback table coordinates: %#v", data) + } + if data["block_name"] != "Orders" { + t.Fatalf("missing table name from fallback: %#v", data) + } + }) + t.Run("base only", func(t *testing.T) { factory, stdout, _ := newExecuteFactory(t) err := runShortcutWithAuthTypes(t, BaseURLResolve, authTypes(), []string{ @@ -269,6 +294,20 @@ func baseBlockListResolveStub(baseToken string, blocks ...map[string]interface{} } } +func tableGetResolveStub(baseToken, tableID, name string) *httpmock.Stub { + return &httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/" + baseToken + "/tables/" + tableID, + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "id": tableID, + "name": name, + }, + }, + } +} + func TestBaseURLResolveWikiURL(t *testing.T) { t.Run("bitable", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) diff --git a/tests/cli_e2e/base/base_url_resolve_dryrun_test.go b/tests/cli_e2e/base/base_url_resolve_dryrun_test.go index 51a7c4420b..d606f1683d 100644 --- a/tests/cli_e2e/base/base_url_resolve_dryrun_test.go +++ b/tests/cli_e2e/base/base_url_resolve_dryrun_test.go @@ -32,6 +32,8 @@ func TestBaseURLResolveSelectedBlockDryRun(t *testing.T) { require.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.0.method").String(), result.Stdout) require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/list", clie2e.DryRunGet(result.Stdout, "api.0.url").String(), result.Stdout) + require.Equal(t, "GET", clie2e.DryRunGet(result.Stdout, "api.1.method").String(), result.Stdout) + require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/blk_selected", clie2e.DryRunGet(result.Stdout, "api.1.url").String(), result.Stdout) require.Equal(t, "blk_selected", clie2e.DryRunGet(result.Stdout, "selected_block_id").String(), result.Stdout) } @@ -58,5 +60,7 @@ func TestBaseURLResolveWikiSelectedBlockDryRun(t *testing.T) { require.Equal(t, "wik_x", clie2e.DryRunGet(result.Stdout, "api.0.params.token").String(), result.Stdout) require.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.1.method").String(), result.Stdout) require.Equal(t, "/open-apis/base/v3/bases/%3Cobj_token%20from%20step%201%3E/blocks/list", clie2e.DryRunGet(result.Stdout, "api.1.url").String(), result.Stdout) + require.Equal(t, "GET", clie2e.DryRunGet(result.Stdout, "api.2.method").String(), result.Stdout) + require.Equal(t, "/open-apis/base/v3/bases/%3Cobj_token%20from%20step%201%3E/tables/wkf_selected", clie2e.DryRunGet(result.Stdout, "api.2.url").String(), result.Stdout) require.Equal(t, "wkf_selected", clie2e.DryRunGet(result.Stdout, "selected_block_id").String(), result.Stdout) }