mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
154ecdb90f
Emit structured validation, API, network, file, and internal error envelopes for Wiki shortcuts so users and agents can recover from failed wiki workflows using stable type, subtype, param, and code fields. Add Wiki domain errscontract and golangci guards to prevent legacy envelope and common helper regressions.
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
|
|
}
|