bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
689 lines
22 KiB
Go
689 lines
22 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package wiki
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var (
|
|
wikiMovePollAttempts = 30
|
|
wikiMovePollInterval = 2 * time.Second
|
|
)
|
|
|
|
const (
|
|
wikiMoveModeNode = "node"
|
|
wikiMoveModeDocsToWiki = "docs_to_wiki"
|
|
)
|
|
|
|
var wikiMoveObjectTypes = []string{
|
|
"doc",
|
|
"sheet",
|
|
"bitable",
|
|
"mindnote",
|
|
"docx",
|
|
"file",
|
|
"slides",
|
|
}
|
|
|
|
// WikiMove moves an existing wiki node inside Wiki or migrates a Drive
|
|
// document into Wiki with bounded polling for async task completion.
|
|
var WikiMove = common.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+move",
|
|
Description: "Move a wiki node, or move a Drive document into Wiki",
|
|
Risk: "write",
|
|
Scopes: []string{"wiki:node:move", "wiki:node:read", "wiki:space:read"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
Flags: []common.Flag{
|
|
{Name: "node-token", Desc: "wiki node token to move inside Wiki"},
|
|
{Name: "source-space-id", Desc: "source wiki space ID for --node-token; if omitted, it is resolved from the node token"},
|
|
{Name: "target-space-id", Desc: "target wiki space ID; required for docs-to-wiki, optional for node move when --target-parent-token is set"},
|
|
{Name: "target-parent-token", Desc: "target parent wiki node token; if omitted for docs-to-wiki, the document is moved to the target space root"},
|
|
{Name: "obj-type", Desc: "Drive document type for docs-to-wiki mode", Enum: wikiMoveObjectTypes},
|
|
{Name: "obj-token", Desc: "Drive document token for docs-to-wiki mode"},
|
|
{Name: "apply", Type: "bool", Desc: "submit a move request when the caller lacks permission to move the document immediately"},
|
|
},
|
|
Tips: []string{
|
|
"Use --node-token to move an existing wiki node inside or across wiki spaces.",
|
|
"Use --obj-type and --obj-token to move a Drive document into Wiki.",
|
|
"If docs-to-wiki returns a long-running task, this command polls for a bounded window and then prints a follow-up drive +task_result command.",
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
spec := readWikiMoveSpec(runtime)
|
|
// `my_library` is a per-user personal-library alias; it has no meaning
|
|
// for a tenant_access_token (--as bot), so reject early with a clear
|
|
// hint instead of letting the API return a confusing error.
|
|
if runtime.As().IsBot() && spec.TargetSpaceID == wikiMyLibrarySpaceID {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-space-id my_library is a per-user personal library alias and cannot be used with --as bot; resolve it to a real space_id first via `lark-cli wiki spaces get --params '{\"space_id\":\"my_library\"}' --as user`").WithParam("--target-space-id")
|
|
}
|
|
return validateWikiMoveSpec(spec)
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
return buildWikiMoveDryRun(readWikiMoveSpec(runtime))
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
spec := readWikiMoveSpec(runtime)
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Running wiki move (%s)...\n", spec.Mode())
|
|
|
|
out, err := runWikiMove(ctx, wikiMoveAPI{runtime: runtime}, runtime, spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
runtime.Out(out, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
type wikiMoveSpec struct {
|
|
NodeToken string
|
|
SourceSpaceID string
|
|
TargetSpaceID string
|
|
TargetParentToken string
|
|
ObjType string
|
|
ObjToken string
|
|
Apply bool
|
|
}
|
|
|
|
func (spec wikiMoveSpec) Mode() string {
|
|
if spec.NodeToken != "" {
|
|
return wikiMoveModeNode
|
|
}
|
|
return wikiMoveModeDocsToWiki
|
|
}
|
|
|
|
func (spec wikiMoveSpec) NodeMoveBody() map[string]interface{} {
|
|
body := map[string]interface{}{}
|
|
if spec.TargetParentToken != "" {
|
|
body["target_parent_token"] = spec.TargetParentToken
|
|
}
|
|
if spec.TargetSpaceID != "" {
|
|
body["target_space_id"] = spec.TargetSpaceID
|
|
}
|
|
return body
|
|
}
|
|
|
|
func (spec wikiMoveSpec) DocsToWikiBody() map[string]interface{} {
|
|
body := map[string]interface{}{
|
|
"obj_type": spec.ObjType,
|
|
"obj_token": spec.ObjToken,
|
|
}
|
|
if spec.TargetParentToken != "" {
|
|
body["parent_wiki_token"] = spec.TargetParentToken
|
|
}
|
|
if spec.Apply {
|
|
body["apply"] = true
|
|
}
|
|
return body
|
|
}
|
|
|
|
type wikiMoveTaskResult struct {
|
|
Node *wikiNodeRecord
|
|
Status int
|
|
StatusMsg string
|
|
}
|
|
|
|
type wikiMoveTaskStatus struct {
|
|
TaskID string
|
|
MoveResults []wikiMoveTaskResult
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) Ready() bool {
|
|
if len(s.MoveResults) == 0 {
|
|
return false
|
|
}
|
|
for _, result := range s.MoveResults {
|
|
if result.Status != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) Failed() bool {
|
|
for _, result := range s.MoveResults {
|
|
if result.Status < 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) Pending() bool {
|
|
return !s.Ready() && !s.Failed()
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) FirstResult() *wikiMoveTaskResult {
|
|
if len(s.MoveResults) == 0 {
|
|
return nil
|
|
}
|
|
return &s.MoveResults[0]
|
|
}
|
|
|
|
// primaryResult picks the most informative move_result for top-level status
|
|
// surfacing: prefer a failing entry so multi-doc tasks don't mask failures
|
|
// behind an earlier success, then a still-processing entry, and finally fall
|
|
// back to the first entry.
|
|
func (s wikiMoveTaskStatus) primaryResult() *wikiMoveTaskResult {
|
|
for i := range s.MoveResults {
|
|
if s.MoveResults[i].Status < 0 {
|
|
return &s.MoveResults[i]
|
|
}
|
|
}
|
|
for i := range s.MoveResults {
|
|
if s.MoveResults[i].Status > 0 {
|
|
return &s.MoveResults[i]
|
|
}
|
|
}
|
|
return s.FirstResult()
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) PrimaryStatusCode() int {
|
|
if r := s.primaryResult(); r != nil {
|
|
return r.Status
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) PrimaryStatusLabel() string {
|
|
if r := s.primaryResult(); r != nil {
|
|
if msg := strings.TrimSpace(r.StatusMsg); msg != "" {
|
|
return msg
|
|
}
|
|
}
|
|
switch {
|
|
case s.Ready():
|
|
return "success"
|
|
case s.Failed():
|
|
return "failure"
|
|
default:
|
|
return "processing"
|
|
}
|
|
}
|
|
|
|
type wikiMoveDocsResponse struct {
|
|
WikiToken string
|
|
TaskID string
|
|
Applied bool
|
|
}
|
|
|
|
type wikiMoveClient interface {
|
|
GetNode(ctx context.Context, token string) (*wikiNodeRecord, error)
|
|
MoveNode(ctx context.Context, sourceSpaceID string, spec wikiMoveSpec) (*wikiNodeRecord, error)
|
|
MoveDocsToWiki(ctx context.Context, targetSpaceID string, spec wikiMoveSpec) (*wikiMoveDocsResponse, error)
|
|
GetMoveTask(ctx context.Context, taskID string) (wikiMoveTaskStatus, error)
|
|
}
|
|
|
|
type wikiMoveAPI struct {
|
|
runtime *common.RuntimeContext
|
|
}
|
|
|
|
func (api wikiMoveAPI) GetNode(ctx context.Context, token string) (*wikiNodeRecord, error) {
|
|
data, err := api.runtime.CallAPITyped(
|
|
"GET",
|
|
"/open-apis/wiki/v2/spaces/get_node",
|
|
map[string]interface{}{"token": token},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseWikiNodeRecord(common.GetMap(data, "node"))
|
|
}
|
|
|
|
func (api wikiMoveAPI) MoveNode(ctx context.Context, sourceSpaceID string, spec wikiMoveSpec) (*wikiNodeRecord, error) {
|
|
data, err := api.runtime.CallAPITyped(
|
|
"POST",
|
|
fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/nodes/%s/move",
|
|
validate.EncodePathSegment(sourceSpaceID),
|
|
validate.EncodePathSegment(spec.NodeToken),
|
|
),
|
|
nil,
|
|
spec.NodeMoveBody(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseWikiNodeRecord(common.GetMap(data, "node"))
|
|
}
|
|
|
|
func (api wikiMoveAPI) MoveDocsToWiki(ctx context.Context, targetSpaceID string, spec wikiMoveSpec) (*wikiMoveDocsResponse, error) {
|
|
data, err := api.runtime.CallAPITyped(
|
|
"POST",
|
|
fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/nodes/move_docs_to_wiki",
|
|
validate.EncodePathSegment(targetSpaceID),
|
|
),
|
|
nil,
|
|
spec.DocsToWikiBody(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &wikiMoveDocsResponse{
|
|
WikiToken: common.GetString(data, "wiki_token"),
|
|
TaskID: common.GetString(data, "task_id"),
|
|
Applied: common.GetBool(data, "applied"),
|
|
}, nil
|
|
}
|
|
|
|
func (api wikiMoveAPI) GetMoveTask(ctx context.Context, taskID string) (wikiMoveTaskStatus, error) {
|
|
data, err := api.runtime.CallAPITyped(
|
|
"GET",
|
|
fmt.Sprintf("/open-apis/wiki/v2/tasks/%s", validate.EncodePathSegment(taskID)),
|
|
map[string]interface{}{"task_type": "move"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return wikiMoveTaskStatus{}, err
|
|
}
|
|
return parseWikiMoveTaskStatus(taskID, common.GetMap(data, "task"))
|
|
}
|
|
|
|
func readWikiMoveSpec(runtime *common.RuntimeContext) wikiMoveSpec {
|
|
return wikiMoveSpec{
|
|
NodeToken: strings.TrimSpace(runtime.Str("node-token")),
|
|
SourceSpaceID: strings.TrimSpace(runtime.Str("source-space-id")),
|
|
TargetSpaceID: strings.TrimSpace(runtime.Str("target-space-id")),
|
|
TargetParentToken: strings.TrimSpace(runtime.Str("target-parent-token")),
|
|
ObjType: strings.ToLower(strings.TrimSpace(runtime.Str("obj-type"))),
|
|
ObjToken: strings.TrimSpace(runtime.Str("obj-token")),
|
|
Apply: runtime.Bool("apply"),
|
|
}
|
|
}
|
|
|
|
func validateWikiMoveSpec(spec wikiMoveSpec) error {
|
|
if err := validateOptionalResourceName(spec.NodeToken, "--node-token"); err != nil {
|
|
return err
|
|
}
|
|
if err := validateOptionalResourceName(spec.SourceSpaceID, "--source-space-id"); err != nil {
|
|
return err
|
|
}
|
|
if err := validateOptionalResourceName(spec.TargetSpaceID, "--target-space-id"); err != nil {
|
|
return err
|
|
}
|
|
if err := validateOptionalResourceName(spec.TargetParentToken, "--target-parent-token"); err != nil {
|
|
return err
|
|
}
|
|
if err := validateOptionalResourceName(spec.ObjToken, "--obj-token"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if spec.NodeToken != "" {
|
|
if spec.ObjType != "" || spec.ObjToken != "" || spec.Apply {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--node-token cannot be combined with --obj-type, --obj-token, or --apply").
|
|
WithParams(
|
|
errs.InvalidParam{Name: "--obj-type", Reason: "cannot be combined with --node-token"},
|
|
errs.InvalidParam{Name: "--obj-token", Reason: "cannot be combined with --node-token"},
|
|
errs.InvalidParam{Name: "--apply", Reason: "cannot be combined with --node-token"},
|
|
)
|
|
}
|
|
if spec.TargetParentToken == "" && spec.TargetSpaceID == "" {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-parent-token and --target-space-id cannot both be empty for wiki node move").
|
|
WithParams(
|
|
errs.InvalidParam{Name: "--target-parent-token", Reason: "provide --target-parent-token or --target-space-id"},
|
|
errs.InvalidParam{Name: "--target-space-id", Reason: "provide --target-parent-token or --target-space-id"},
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if spec.SourceSpaceID != "" {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--source-space-id can only be used with --node-token").WithParam("--source-space-id")
|
|
}
|
|
if spec.ObjType == "" && spec.ObjToken == "" && !spec.Apply {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "provide --node-token for wiki node move, or provide --obj-type and --obj-token for docs-to-wiki move").
|
|
WithParams(
|
|
errs.InvalidParam{Name: "--node-token", Reason: "provide --node-token, or --obj-type and --obj-token"},
|
|
errs.InvalidParam{Name: "--obj-type", Reason: "provide --node-token, or --obj-type and --obj-token"},
|
|
errs.InvalidParam{Name: "--obj-token", Reason: "provide --node-token, or --obj-type and --obj-token"},
|
|
)
|
|
}
|
|
if spec.ObjType == "" {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--obj-type is required for docs-to-wiki move").WithParam("--obj-type")
|
|
}
|
|
if spec.ObjToken == "" {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--obj-token is required for docs-to-wiki move").WithParam("--obj-token")
|
|
}
|
|
if spec.TargetSpaceID == "" {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-space-id is required for docs-to-wiki move").WithParam("--target-space-id")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func buildWikiMoveDryRun(spec wikiMoveSpec) *common.DryRunAPI {
|
|
dry := common.NewDryRunAPI()
|
|
switch spec.Mode() {
|
|
case wikiMoveModeNode:
|
|
step := 1
|
|
switch {
|
|
case spec.SourceSpaceID == "" && spec.TargetParentToken != "":
|
|
dry.Desc("3-step orchestration: resolve source node -> resolve target parent -> move wiki node")
|
|
case spec.SourceSpaceID == "":
|
|
dry.Desc("2-step orchestration: resolve source node -> move wiki node")
|
|
case spec.TargetParentToken != "":
|
|
dry.Desc("2-step orchestration: resolve target parent -> move wiki node")
|
|
default:
|
|
dry.Desc("1-step request: move wiki node")
|
|
}
|
|
|
|
if spec.SourceSpaceID == "" {
|
|
dry.GET("/open-apis/wiki/v2/spaces/get_node").
|
|
Desc(fmt.Sprintf("[%d] Resolve source space from node token", step)).
|
|
Params(map[string]interface{}{"token": spec.NodeToken})
|
|
step++
|
|
}
|
|
if spec.TargetParentToken != "" {
|
|
dry.GET("/open-apis/wiki/v2/spaces/get_node").
|
|
Desc(fmt.Sprintf("[%d] Resolve target parent node", step)).
|
|
Params(map[string]interface{}{"token": spec.TargetParentToken})
|
|
step++
|
|
}
|
|
|
|
dry.POST(fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/nodes/%s/move",
|
|
dryRunWikiMoveSourceSpaceID(spec),
|
|
validate.EncodePathSegment(spec.NodeToken),
|
|
)).
|
|
Desc(fmt.Sprintf("[%d] Move wiki node", step)).
|
|
Body(spec.NodeMoveBody())
|
|
case wikiMoveModeDocsToWiki:
|
|
dry.Desc("2-step orchestration: move Drive document into Wiki -> poll wiki task result when task_id is returned")
|
|
dry.POST(fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/nodes/move_docs_to_wiki",
|
|
dryRunWikiMoveTargetSpaceID(spec),
|
|
)).
|
|
Desc("[1] Move Drive document into Wiki").
|
|
Body(spec.DocsToWikiBody())
|
|
dry.GET("/open-apis/wiki/v2/tasks/:task_id").
|
|
Desc("[2] Poll wiki move task result when async").
|
|
Set("task_id", "<task_id>").
|
|
Params(map[string]interface{}{"task_type": "move"})
|
|
default:
|
|
dry.Set("error", "unknown wiki move mode")
|
|
}
|
|
return dry
|
|
}
|
|
|
|
func dryRunWikiMoveSourceSpaceID(spec wikiMoveSpec) string {
|
|
if spec.SourceSpaceID != "" {
|
|
return validate.EncodePathSegment(spec.SourceSpaceID)
|
|
}
|
|
return "<resolved_source_space_id>"
|
|
}
|
|
|
|
func dryRunWikiMoveTargetSpaceID(spec wikiMoveSpec) string {
|
|
if spec.TargetSpaceID != "" {
|
|
return validate.EncodePathSegment(spec.TargetSpaceID)
|
|
}
|
|
return "<target_space_id>"
|
|
}
|
|
|
|
func runWikiMove(ctx context.Context, client wikiMoveClient, runtime *common.RuntimeContext, spec wikiMoveSpec) (map[string]interface{}, error) {
|
|
switch spec.Mode() {
|
|
case wikiMoveModeNode:
|
|
return runWikiNodeMove(ctx, client, spec)
|
|
case wikiMoveModeDocsToWiki:
|
|
return runWikiDocsToWikiMove(ctx, client, runtime, spec)
|
|
default:
|
|
return nil, errs.NewInternalError(errs.SubtypeUnknown, "unknown wiki move mode")
|
|
}
|
|
}
|
|
|
|
func runWikiNodeMove(ctx context.Context, client wikiMoveClient, spec wikiMoveSpec) (map[string]interface{}, error) {
|
|
sourceSpaceID, targetSpaceID, err := resolveWikiNodeMoveSpaces(ctx, client, spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
node, err := client.MoveNode(ctx, sourceSpaceID, spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := map[string]interface{}{
|
|
"mode": wikiMoveModeNode,
|
|
"source_space_id": sourceSpaceID,
|
|
"target_space_id": targetSpaceID,
|
|
}
|
|
appendWikiNodeOutput(out, node)
|
|
return out, nil
|
|
}
|
|
|
|
func resolveWikiNodeMoveSpaces(ctx context.Context, client wikiMoveClient, spec wikiMoveSpec) (string, string, error) {
|
|
// Node move requests may start from just a node token and/or a target parent.
|
|
// Resolve both ends up front so we can fail on space mismatches before sending
|
|
// the mutation request.
|
|
sourceSpaceID := spec.SourceSpaceID
|
|
if sourceSpaceID == "" {
|
|
sourceNode, err := client.GetNode(ctx, spec.NodeToken)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
sourceSpaceID, err = requireWikiNodeSpaceID(sourceNode)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
targetSpaceID := spec.TargetSpaceID
|
|
if spec.TargetParentToken != "" {
|
|
targetParent, err := client.GetNode(ctx, spec.TargetParentToken)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
parentSpaceID, err := requireWikiNodeSpaceID(targetParent)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if targetSpaceID == "" {
|
|
targetSpaceID = parentSpaceID
|
|
} else if targetSpaceID != parentSpaceID {
|
|
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument,
|
|
"--target-space-id %q does not match target parent node space %q",
|
|
spec.TargetSpaceID,
|
|
parentSpaceID,
|
|
).WithParam("--target-space-id")
|
|
}
|
|
}
|
|
|
|
if targetSpaceID == "" {
|
|
targetSpaceID = sourceSpaceID
|
|
}
|
|
|
|
return sourceSpaceID, targetSpaceID, nil
|
|
}
|
|
|
|
func runWikiDocsToWikiMove(ctx context.Context, client wikiMoveClient, runtime *common.RuntimeContext, spec wikiMoveSpec) (map[string]interface{}, error) {
|
|
response, err := client.MoveDocsToWiki(ctx, spec.TargetSpaceID, spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := map[string]interface{}{
|
|
"mode": wikiMoveModeDocsToWiki,
|
|
"obj_type": spec.ObjType,
|
|
"obj_token": spec.ObjToken,
|
|
"target_space_id": spec.TargetSpaceID,
|
|
"target_parent_token": spec.TargetParentToken,
|
|
}
|
|
|
|
// move_docs_to_wiki has three success-shaped responses: immediate completion,
|
|
// approval-request submission, or an async task that must be polled.
|
|
switch {
|
|
case response.WikiToken != "":
|
|
out["ready"] = true
|
|
out["failed"] = false
|
|
out["wiki_token"] = response.WikiToken
|
|
out["node_token"] = response.WikiToken
|
|
return out, nil
|
|
case response.Applied:
|
|
out["ready"] = false
|
|
out["failed"] = false
|
|
out["applied"] = true
|
|
out["status_msg"] = "move request submitted for approval"
|
|
return out, nil
|
|
case response.TaskID != "":
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Docs-to-wiki move is async, polling task %s...\n", response.TaskID)
|
|
status, ready, err := pollWikiMoveTask(ctx, client, runtime, response.TaskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out["task_id"] = response.TaskID
|
|
out["ready"] = ready
|
|
out["failed"] = status.Failed()
|
|
out["status"] = status.PrimaryStatusCode()
|
|
out["status_msg"] = status.PrimaryStatusLabel()
|
|
if first := status.FirstResult(); first != nil {
|
|
appendWikiNodeOutput(out, first.Node)
|
|
if first.Node != nil && first.Node.NodeToken != "" {
|
|
out["wiki_token"] = first.Node.NodeToken
|
|
}
|
|
}
|
|
if !ready {
|
|
nextCommand := wikiMoveTaskResultCommand(response.TaskID, runtime.As())
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Wiki move task is still in progress. Continue with: %s\n", nextCommand)
|
|
out["timed_out"] = true
|
|
out["next_command"] = nextCommand
|
|
}
|
|
return out, nil
|
|
default:
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "move_docs_to_wiki returned neither wiki_token, task_id, nor applied result")
|
|
}
|
|
}
|
|
|
|
func wikiMoveTaskResultCommand(taskID string, identity core.Identity) string {
|
|
asFlag := string(identity)
|
|
if asFlag == "" {
|
|
asFlag = "user"
|
|
}
|
|
return fmt.Sprintf("lark-cli drive +task_result --scenario wiki_move --task-id %s --as %s", taskID, asFlag)
|
|
}
|
|
|
|
func pollWikiMoveTask(ctx context.Context, client wikiMoveClient, runtime *common.RuntimeContext, taskID string) (wikiMoveTaskStatus, bool, error) {
|
|
lastStatus := wikiMoveTaskStatus{TaskID: taskID}
|
|
var lastErr error
|
|
hadSuccessfulPoll := false
|
|
|
|
// The move request itself already succeeded. Treat poll failures as transient
|
|
// until every attempt fails, then return a resume hint instead of discarding
|
|
// the task identifier.
|
|
for attempt := 1; attempt <= wikiMovePollAttempts; attempt++ {
|
|
if attempt > 1 {
|
|
select {
|
|
case <-ctx.Done():
|
|
return lastStatus, false, ctx.Err()
|
|
case <-time.After(wikiMovePollInterval):
|
|
}
|
|
}
|
|
|
|
status, err := client.GetMoveTask(ctx, taskID)
|
|
if err != nil {
|
|
lastErr = err
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Wiki move status attempt %d/%d failed: %v\n", attempt, wikiMovePollAttempts, err)
|
|
continue
|
|
}
|
|
lastStatus = status
|
|
hadSuccessfulPoll = true
|
|
|
|
if status.Ready() {
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Wiki move task completed successfully.\n")
|
|
return status, true, nil
|
|
}
|
|
if status.Failed() {
|
|
return status, false, errs.NewAPIError(errs.SubtypeServerError, "wiki move task failed: %s", status.PrimaryStatusLabel())
|
|
}
|
|
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Wiki move status %d/%d: %s\n", attempt, wikiMovePollAttempts, status.PrimaryStatusLabel())
|
|
}
|
|
|
|
if !hadSuccessfulPoll && lastErr != nil {
|
|
nextCommand := wikiMoveTaskResultCommand(taskID, runtime.As())
|
|
hint := fmt.Sprintf(
|
|
"the wiki move task was created but every status poll failed (task_id=%s)\nretry status lookup with: %s",
|
|
taskID,
|
|
nextCommand,
|
|
)
|
|
// The poll error comes from a typed CallAPITyped path; append the resume
|
|
// hint in place so the original category / subtype / code / log_id
|
|
// survives a fully failed poll (per ERROR_CONTRACT.md "propagate typed
|
|
// errors unchanged").
|
|
if p, ok := errs.ProblemOf(lastErr); ok {
|
|
if strings.TrimSpace(p.Hint) != "" {
|
|
hint = p.Hint + "\n" + hint
|
|
}
|
|
p.Hint = hint
|
|
return lastStatus, false, lastErr
|
|
}
|
|
return lastStatus, false, errs.NewInternalError(errs.SubtypeSDKError, "%s", lastErr.Error()).WithHint("%s", hint).WithCause(lastErr)
|
|
}
|
|
|
|
return lastStatus, false, nil
|
|
}
|
|
|
|
func parseWikiMoveTaskStatus(taskID string, task map[string]interface{}) (wikiMoveTaskStatus, error) {
|
|
if task == nil {
|
|
return wikiMoveTaskStatus{}, errs.NewInternalError(errs.SubtypeInvalidResponse, "wiki task response missing task")
|
|
}
|
|
|
|
status := wikiMoveTaskStatus{
|
|
TaskID: common.GetString(task, "task_id"),
|
|
}
|
|
if status.TaskID == "" {
|
|
status.TaskID = taskID
|
|
}
|
|
|
|
for _, item := range common.GetSlice(task, "move_result") {
|
|
resultMap, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
var node *wikiNodeRecord
|
|
if nodeMap := common.GetMap(resultMap, "node"); nodeMap != nil {
|
|
parsedNode, err := parseWikiNodeRecord(nodeMap)
|
|
if err != nil {
|
|
return wikiMoveTaskStatus{}, err
|
|
}
|
|
node = parsedNode
|
|
}
|
|
|
|
status.MoveResults = append(status.MoveResults, wikiMoveTaskResult{
|
|
Node: node,
|
|
Status: int(common.GetFloat(resultMap, "status")),
|
|
StatusMsg: common.GetString(resultMap, "status_msg"),
|
|
})
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
func appendWikiNodeOutput(out map[string]interface{}, node *wikiNodeRecord) {
|
|
if out == nil || node == nil {
|
|
return
|
|
}
|
|
out["space_id"] = node.SpaceID
|
|
out["node_token"] = node.NodeToken
|
|
out["obj_token"] = node.ObjToken
|
|
out["obj_type"] = node.ObjType
|
|
out["parent_node_token"] = node.ParentNodeToken
|
|
out["node_type"] = node.NodeType
|
|
out["origin_node_token"] = node.OriginNodeToken
|
|
out["title"] = node.Title
|
|
out["has_child"] = node.HasChild
|
|
}
|