Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions shortcuts/wiki/wiki_helpers.go
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 4 additions & 0 deletions shortcuts/wiki/wiki_list_copy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions shortcuts/wiki/wiki_node_copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Comment thread
fangshuyu-768 marked this conversation as resolved.
out["url"] = u
}
runtime.OutFormat(out, nil, func(w io.Writer) {
renderWikiNodeCopyPretty(w, out)
})
Expand All @@ -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{} {
Expand Down
4 changes: 3 additions & 1 deletion shortcuts/wiki/wiki_node_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ type wikiNodeRecord struct {
OriginNodeToken string
Title string
HasChild bool
URL string
}

// wikiSpaceRecord contains the response fields used when resolving spaces.
Expand Down Expand Up @@ -456,6 +457,7 @@ 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
}

Expand Down Expand Up @@ -498,7 +500,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
Expand Down
49 changes: 47 additions & 2 deletions shortcuts/wiki/wiki_node_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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",
Comment thread
liujinkun2025 marked this conversation as resolved.
},
{
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)
}
})
}
}
Loading