From 82e61019726b064c04eb85ed9645dfe031207d45 Mon Sep 17 00:00:00 2001 From: liujinkun Date: Tue, 19 May 2026 12:47:12 +0800 Subject: [PATCH 1/2] fix(wiki): surface real node url for +node-create / +node-copy The create-node and copy-node OpenAPI responses carry a real `url` field (present in practice though absent from the documented schema). Both shortcuts ignored it: +node-create synthesized a link via BuildResourceURL, and +node-copy emitted no URL at all. Parse `url` into the shared wikiNodeRecord and add a wikiNodeURL helper that prefers the response url, falling back to BuildResourceURL only when it is blank. Wire +node-create and +node-copy to the helper so both surface the canonical link when available. Change-Id: I0ca5f91b02c24e81d083793e6a8e4f8c966aeec3 --- shortcuts/wiki/wiki_list_copy_test.go | 4 ++ shortcuts/wiki/wiki_node_copy.go | 6 +++ shortcuts/wiki/wiki_node_create.go | 18 ++++++++- shortcuts/wiki/wiki_node_create_test.go | 49 ++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/shortcuts/wiki/wiki_list_copy_test.go b/shortcuts/wiki/wiki_list_copy_test.go index 6c9cf07d4e..e5bbf31f5e 100644 --- a/shortcuts/wiki/wiki_list_copy_test.go +++ b/shortcuts/wiki/wiki_list_copy_test.go @@ -415,6 +415,7 @@ func TestWikiNodeCopyCopiesNodeToTargetSpace(t *testing.T) { "node_type": "origin", "title": "Architecture (Copy)", "has_child": false, + "url": "https://abc.feishu.cn/wiki/wik_copied_real", }, }, "msg": "success", @@ -451,6 +452,9 @@ func TestWikiNodeCopyCopiesNodeToTargetSpace(t *testing.T) { if envelope.Data["space_id"] != "space_dst" { t.Fatalf("space_id = %v, want %q", envelope.Data["space_id"], "space_dst") } + if got, want := envelope.Data["url"], "https://abc.feishu.cn/wiki/wik_copied_real"; got != want { + t.Fatalf("url = %#v, want %q (copy must surface the response url)", got, want) + } var captured map[string]interface{} if err := json.Unmarshal(stub.CapturedBody, &captured); err != nil { diff --git a/shortcuts/wiki/wiki_node_copy.go b/shortcuts/wiki/wiki_node_copy.go index 5a4a97d004..c1bea07cc8 100644 --- a/shortcuts/wiki/wiki_node_copy.go +++ b/shortcuts/wiki/wiki_node_copy.go @@ -89,6 +89,9 @@ var WikiNodeCopy = common.Shortcut{ fmt.Fprintf(runtime.IO().ErrOut, "Copied to node %s in space %s\n", common.MaskToken(node.NodeToken), common.MaskToken(node.SpaceID)) out := wikiNodeCopyOutput(node) + if u := wikiNodeURL(runtime.Config.Brand, node); u != "" { + out["url"] = u + } runtime.OutFormat(out, nil, func(w io.Writer) { renderWikiNodeCopyPretty(w, out) }) @@ -106,6 +109,9 @@ func renderWikiNodeCopyPretty(w io.Writer, out map[string]interface{}) { if parent, _ := out["parent_node_token"].(string); parent != "" { fmt.Fprintf(w, " parent_node_token: %s\n", parent) } + if url, _ := out["url"].(string); url != "" { + fmt.Fprintf(w, " url: %s\n", url) + } } func buildNodeCopyBody(runtime *common.RuntimeContext) map[string]interface{} { diff --git a/shortcuts/wiki/wiki_node_create.go b/shortcuts/wiki/wiki_node_create.go index d9fd7e5adc..e439a9311e 100644 --- a/shortcuts/wiki/wiki_node_create.go +++ b/shortcuts/wiki/wiki_node_create.go @@ -118,6 +118,7 @@ type wikiNodeRecord struct { OriginNodeToken string Title string HasChild bool + URL string } // wikiSpaceRecord contains the response fields used when resolving spaces. @@ -456,9 +457,24 @@ func parseWikiNodeRecord(node map[string]interface{}) (*wikiNodeRecord, error) { OriginNodeToken: common.GetString(node, "origin_node_token"), Title: common.GetString(node, "title"), HasChild: common.GetBool(node, "has_child"), + URL: common.GetString(node, "url"), }, nil } +// wikiNodeURL returns the user-facing link for a wiki node. The create/copy +// OpenAPI responses carry a real `url` (undocumented in the server-docs schema +// but present in practice); prefer it so the CLI surfaces the canonical link. +// Fall back to BuildResourceURL synthesis only when the response omits it. +func wikiNodeURL(brand core.LarkBrand, node *wikiNodeRecord) string { + if node == nil { + return "" + } + if u := strings.TrimSpace(node.URL); u != "" { + return u + } + return common.BuildResourceURL(brand, "wiki", node.NodeToken) +} + func parseWikiSpaceRecord(space map[string]interface{}) (*wikiSpaceRecord, error) { if space == nil { return nil, output.Errorf(output.ExitAPI, "api_error", "wiki space response missing space") @@ -498,7 +514,7 @@ func augmentWikiNodeCreateOutput(runtime *common.RuntimeContext, execution *wiki if grant := common.AutoGrantCurrentUserDrivePermission(runtime, execution.Node.NodeToken, "wiki"); grant != nil { out["permission_grant"] = grant } - if u := common.BuildResourceURL(runtime.Config.Brand, "wiki", execution.Node.NodeToken); u != "" { + if u := wikiNodeURL(runtime.Config.Brand, execution.Node); u != "" { out["url"] = u } return out diff --git a/shortcuts/wiki/wiki_node_create_test.go b/shortcuts/wiki/wiki_node_create_test.go index 3cdab33c08..573c47e433 100644 --- a/shortcuts/wiki/wiki_node_create_test.go +++ b/shortcuts/wiki/wiki_node_create_test.go @@ -451,6 +451,7 @@ func TestWikiNodeCreateMountedExecuteWithExplicitSpaceID(t *testing.T) { "origin_node_token": "", "title": "Wiki Node", "has_child": false, + "url": "https://abc.feishu.cn/wiki/wik_created_real", }, }, "msg": "success", @@ -484,8 +485,8 @@ func TestWikiNodeCreateMountedExecuteWithExplicitSpaceID(t *testing.T) { if envelope.Data["node_token"] != "wik_created" { t.Fatalf("node_token = %#v, want %q", envelope.Data["node_token"], "wik_created") } - if got, want := envelope.Data["url"], "https://www.feishu.cn/wiki/wik_created"; got != want { - t.Fatalf("url = %#v, want %q", got, want) + if got, want := envelope.Data["url"], "https://abc.feishu.cn/wiki/wik_created_real"; got != want { + t.Fatalf("url = %#v, want %q (response url must win over synthesized fallback)", got, want) } var captured map[string]interface{} @@ -628,3 +629,47 @@ func TestAugmentWikiNodeCreateOutputReturnsEmptyMapForNilInput(t *testing.T) { t.Fatalf("augmentWikiNodeCreateOutput(nil, empty execution) = %#v, want empty map", got) } } + +func TestWikiNodeURL(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + node *wikiNodeRecord + want string + }{ + { + name: "prefers response url over synthesized fallback", + node: &wikiNodeRecord{NodeToken: "wik_token", URL: "https://abc.feishu.cn/wiki/wik_real"}, + want: "https://abc.feishu.cn/wiki/wik_real", + }, + { + name: "falls back to synthesized url when response omits it", + node: &wikiNodeRecord{NodeToken: "wik_token"}, + want: "https://www.feishu.cn/wiki/wik_token", + }, + { + name: "blank response url is treated as absent", + node: &wikiNodeRecord{NodeToken: "wik_token", URL: " "}, + want: "https://www.feishu.cn/wiki/wik_token", + }, + { + name: "nil node yields empty string", + node: nil, + want: "", + }, + { + name: "no token and no url yields empty string", + node: &wikiNodeRecord{}, + want: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := wikiNodeURL(core.BrandFeishu, tc.node); got != tc.want { + t.Fatalf("wikiNodeURL() = %q, want %q", got, tc.want) + } + }) + } +} From 13ca095f8a4e9c24b97ed84e587582ae57f6183c Mon Sep 17 00:00:00 2001 From: liujinkun Date: Tue, 19 May 2026 14:07:01 +0800 Subject: [PATCH 2/2] refactor(wiki): move wikiNodeURL to shared wiki_helpers.go The helper is consumed by both +node-create and +node-copy, so its placement should reflect the broader usage rather than living in the create command's file. Pure move; no behavior change. Change-Id: I9990c12da042f631fe2519911c6a9d663fd5c22b --- shortcuts/wiki/wiki_helpers.go | 28 ++++++++++++++++++++++++++++ shortcuts/wiki/wiki_node_create.go | 14 -------------- 2 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 shortcuts/wiki/wiki_helpers.go diff --git a/shortcuts/wiki/wiki_helpers.go b/shortcuts/wiki/wiki_helpers.go new file mode 100644 index 0000000000..e69184803a --- /dev/null +++ b/shortcuts/wiki/wiki_helpers.go @@ -0,0 +1,28 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package wiki + +import ( + "strings" + + "github.com/larksuite/cli/internal/core" + "github.com/larksuite/cli/shortcuts/common" +) + +// wikiNodeURL returns the user-facing link for a wiki node. The create/copy +// OpenAPI responses carry a real `url` (undocumented in the server-docs schema +// but present in practice); prefer it so the CLI surfaces the canonical link. +// Fall back to BuildResourceURL synthesis only when the response omits it. +// +// Shared by +node-create and +node-copy, hence kept here rather than in either +// command's file. +func wikiNodeURL(brand core.LarkBrand, node *wikiNodeRecord) string { + if node == nil { + return "" + } + if u := strings.TrimSpace(node.URL); u != "" { + return u + } + return common.BuildResourceURL(brand, "wiki", node.NodeToken) +} diff --git a/shortcuts/wiki/wiki_node_create.go b/shortcuts/wiki/wiki_node_create.go index e439a9311e..d9b7273173 100644 --- a/shortcuts/wiki/wiki_node_create.go +++ b/shortcuts/wiki/wiki_node_create.go @@ -461,20 +461,6 @@ func parseWikiNodeRecord(node map[string]interface{}) (*wikiNodeRecord, error) { }, nil } -// wikiNodeURL returns the user-facing link for a wiki node. The create/copy -// OpenAPI responses carry a real `url` (undocumented in the server-docs schema -// but present in practice); prefer it so the CLI surfaces the canonical link. -// Fall back to BuildResourceURL synthesis only when the response omits it. -func wikiNodeURL(brand core.LarkBrand, node *wikiNodeRecord) string { - if node == nil { - return "" - } - if u := strings.TrimSpace(node.URL); u != "" { - return u - } - return common.BuildResourceURL(brand, "wiki", node.NodeToken) -} - func parseWikiSpaceRecord(space map[string]interface{}) (*wikiSpaceRecord, error) { if space == nil { return nil, output.Errorf(output.ExitAPI, "api_error", "wiki space response missing space")