mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
98173ae5a9
Drive-domain errors now leave the CLI as typed, machine-branchable envelopes — a stable `type` plus `subtype` and named fields (param, params, retryable, log_id, hint) — so scripts and AI agents can branch on structure and act on a recovery hint instead of parsing prose. Changes: - Every error produced in the drive domain — validation, file I/O, and the failures returned from its Lark API calls — is emitted as a typed errs.* error; the exit code is derived from the error category. Drive's API calls now go through a shared typed classifier, so failures carry subtype, troubleshooter, a recovery hint, and the request's log_id whether the server returns it in the response body or the x-tt-logid header; an already-typed network/auth error is never downgraded into a generic API error. - Known API conditions (resource conflict, cross-tenant, cross-brand, ...) carry a recovery hint keyed by their error class; a command can refine that hint with command-specific guidance. - Batch partial failures (+push / +pull / +sync, where some items succeed and some fail) now report an honest ok:false multi-status result on stdout — the summary and every per-item outcome stay machine-readable — and exit non-zero, instead of a misleading ok:true success envelope. - Duplicate rel_path conflicts report each colliding path as a structured params entry (RFC 7807 invalid-params style). - Static guards lock the drive path so legacy error construction — direct envelopes or the auto-classifying API helpers — cannot be reintroduced, making drive the template for the remaining domains. Output changes worth noting for consumers: - Error envelopes now carry typed type/subtype and named fields; exit codes follow the error category (malformed or incomplete API responses are reported as internal errors rather than generic API errors). - Batch partial failures (+push / +pull / +sync) emit an ok:false result envelope on stdout (summary + per-item items[]) and exit non-zero; the per-item results stay on stdout rather than in a stderr error envelope. Errors surfaced through shared cross-domain helpers (scope precheck, media import upload, metadata lookup, save-path resolution) are not yet typed; they migrate with the shared layer in a follow-up change.
151 lines
5.2 KiB
Go
151 lines
5.2 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package drive
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// permApplyTypes is the authoritative list of type values the apply-permission
|
|
// endpoint accepts for its required `type` query parameter.
|
|
var permApplyTypes = []string{
|
|
"doc", "sheet", "file", "wiki", "bitable", "docx",
|
|
"mindnote", "slides",
|
|
}
|
|
|
|
// permApplyURLMarkers maps document URL path markers to the `type` value the
|
|
// apply-permission endpoint expects. Markers are disjoint strings (each begins
|
|
// with "/" and ends with "/"), so a simple substring scan disambiguates them.
|
|
var permApplyURLMarkers = []struct {
|
|
Marker string
|
|
Type string
|
|
}{
|
|
{"/wiki/", "wiki"},
|
|
{"/docx/", "docx"},
|
|
{"/sheets/", "sheet"},
|
|
{"/base/", "bitable"},
|
|
{"/bitable/", "bitable"},
|
|
{"/file/", "file"},
|
|
{"/mindnote/", "mindnote"},
|
|
{"/slides/", "slides"},
|
|
{"/doc/", "doc"},
|
|
}
|
|
|
|
// resolvePermApplyTarget extracts (token, type) from a user-supplied --token
|
|
// value that may be either a bare token or a full document URL, plus an
|
|
// optional explicit --type. Explicit --type wins over URL inference.
|
|
func resolvePermApplyTarget(raw, explicitType string) (token, docType string, err error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--token is required").WithParam("--token")
|
|
}
|
|
|
|
if strings.Contains(raw, "://") {
|
|
for _, m := range permApplyURLMarkers {
|
|
if tok, ok := extractURLToken(raw, m.Marker); ok {
|
|
token = tok
|
|
if explicitType == "" {
|
|
docType = m.Type
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if token == "" {
|
|
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument,
|
|
"could not infer token from URL %q: supported paths are /docx/, /sheets/, /base/, /bitable/, /file/, /wiki/, /doc/, /mindnote/, /slides/. Pass a bare token with --type instead if the URL shape is unusual",
|
|
raw,
|
|
).WithParam("--token")
|
|
}
|
|
} else {
|
|
token = raw
|
|
}
|
|
|
|
if explicitType != "" {
|
|
docType = explicitType
|
|
}
|
|
if docType == "" {
|
|
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument,
|
|
"--type is required when --token is a bare token; accepted values: %s",
|
|
strings.Join(permApplyTypes, ", "),
|
|
).WithParam("--type")
|
|
}
|
|
return token, docType, nil
|
|
}
|
|
|
|
// DriveApplyPermission applies to the document owner for view or edit access
|
|
// on behalf of the invoking user. Matches the open-apis endpoint
|
|
// /open-apis/drive/v1/permissions/:token/members/apply.
|
|
//
|
|
// The backend accepts only user_access_token for this endpoint, so the
|
|
// shortcut declares AuthTypes: ["user"] — bot identity is rejected up-front.
|
|
var DriveApplyPermission = common.Shortcut{
|
|
Service: "drive",
|
|
Command: "+apply-permission",
|
|
Description: "Apply to the document owner for view or edit permission on a doc/sheet/file/wiki/bitable/docx/mindnote/slides",
|
|
Risk: "write",
|
|
Scopes: []string{"docs:permission.member:apply"},
|
|
AuthTypes: []string{"user"},
|
|
Flags: []common.Flag{
|
|
{Name: "token", Desc: "target token or document URL (docx/sheets/base/file/wiki/doc/mindnote/slides)", Required: true},
|
|
{Name: "type", Desc: "target type; auto-inferred from URL when omitted", Enum: permApplyTypes},
|
|
{Name: "perm", Desc: "permission to request", Required: true, Enum: []string{"view", "edit"}},
|
|
{Name: "remark", Desc: "optional note shown on the request card sent to the owner"},
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
_, _, err := resolvePermApplyTarget(runtime.Str("token"), runtime.Str("type"))
|
|
return err
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token, docType, err := resolvePermApplyTarget(runtime.Str("token"), runtime.Str("type"))
|
|
if err != nil {
|
|
return common.NewDryRunAPI().Set("error", err.Error())
|
|
}
|
|
body := buildPermApplyBody(runtime)
|
|
return common.NewDryRunAPI().
|
|
Desc("Apply to document owner for access").
|
|
POST("/open-apis/drive/v1/permissions/:token/members/apply").
|
|
Params(map[string]interface{}{"type": docType}).
|
|
Body(body).
|
|
Set("token", token)
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
token, docType, err := resolvePermApplyTarget(runtime.Str("token"), runtime.Str("type"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := buildPermApplyBody(runtime)
|
|
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Requesting %s access on %s %s...\n",
|
|
runtime.Str("perm"), docType, common.MaskToken(token))
|
|
|
|
data, err := runtime.CallAPITyped("POST",
|
|
fmt.Sprintf("/open-apis/drive/v1/permissions/%s/members/apply", validate.EncodePathSegment(token)),
|
|
map[string]interface{}{"type": docType},
|
|
body,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtime.Out(data, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// buildPermApplyBody returns the request body with the caller-supplied perm
|
|
// and optional remark. remark is omitted entirely when empty so the server
|
|
// doesn't render an empty note on the request card.
|
|
func buildPermApplyBody(runtime *common.RuntimeContext) map[string]interface{} {
|
|
body := map[string]interface{}{"perm": runtime.Str("perm")}
|
|
if s := runtime.Str("remark"); s != "" {
|
|
body["remark"] = s
|
|
}
|
|
return body
|
|
}
|