mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
a6f3e635d4
* feat(docs): add local authoring and resource workflows Add docs +script workflows for isolated draft initialization and tolerant XML/Markdown profiling. Support local and remote document resources across create and update flows with safe, bounded-concurrency uploads, binding verification, and cleanup. Synchronize shared credential-source selection during concurrent uploads, refresh lark-doc guidance, and expand unit, dry-run, and live E2E coverage. * docs(lark-doc): clarify genre reference paths * fix(docs): address PR validation feedback * docs(lark-doc): clarify remote image handling * feat: streamline docs draft workflow * fix(docs): clarify script input and resource cleanup * fix(docs): align script dry-run test with auth flow * fix(docs): authenticate local script e2e test * fix(docs): refine script diagnostics and image preflight * fix(docs): align draft workspace cleanup with VFS * fix(docs): route workspace cleanup through FileIO * docs(lark-doc): simplify profile check guidance
133 lines
4.0 KiB
Go
133 lines
4.0 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package doc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// docsSceneContextKey lets in-process embedders pass a server-owned docs_ai
|
|
// scene without exposing it as a user-controlled CLI flag.
|
|
const docsSceneContextKey = "lark_cli_docs_scene"
|
|
|
|
type documentRef struct {
|
|
Kind string
|
|
Token string
|
|
Fragment string
|
|
}
|
|
|
|
func parseDocumentRef(input string) (documentRef, error) {
|
|
raw := strings.TrimSpace(input)
|
|
if raw == "" {
|
|
return documentRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--doc cannot be empty").WithParam("--doc")
|
|
}
|
|
|
|
if token, ok := extractDocumentToken(raw, "/wiki/"); ok {
|
|
return documentRef{Kind: "wiki", Token: token, Fragment: extractDocumentFragment(raw)}, nil
|
|
}
|
|
if token, ok := extractDocumentToken(raw, "/docx/"); ok {
|
|
return documentRef{Kind: "docx", Token: token, Fragment: extractDocumentFragment(raw)}, nil
|
|
}
|
|
if token, ok := extractDocumentToken(raw, "/doc/"); ok {
|
|
return documentRef{Kind: "doc", Token: token, Fragment: extractDocumentFragment(raw)}, nil
|
|
}
|
|
if strings.Contains(raw, "://") {
|
|
return documentRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "unsupported --doc input %q: use a docx URL/token or a wiki URL that resolves to docx", raw).WithParam("--doc")
|
|
}
|
|
if strings.ContainsAny(raw, "/?#") {
|
|
return documentRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "unsupported --doc input %q: use a docx token or a wiki URL", raw).WithParam("--doc")
|
|
}
|
|
|
|
return documentRef{Kind: "docx", Token: raw}, nil
|
|
}
|
|
|
|
func extractDocumentToken(raw, marker string) (string, bool) {
|
|
idx := strings.Index(raw, marker)
|
|
if idx < 0 {
|
|
return "", false
|
|
}
|
|
token := raw[idx+len(marker):]
|
|
if end := strings.IndexAny(token, "/?#"); end >= 0 {
|
|
token = token[:end]
|
|
}
|
|
token = strings.TrimSpace(token)
|
|
if token == "" {
|
|
return "", false
|
|
}
|
|
return token, true
|
|
}
|
|
|
|
func extractDocumentFragment(raw string) string {
|
|
idx := strings.Index(raw, "#")
|
|
if idx < 0 {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(raw[idx+1:])
|
|
}
|
|
|
|
// doDocAPI executes an OpenAPI request against the docs_ai endpoints and returns
|
|
// the parsed "data" field from the standard Lark response envelope {code, msg, data}.
|
|
// CallAPITyped lifts the x-tt-logid response header onto the typed error so log_id
|
|
// surfaces for support escalations even when the body omits it.
|
|
func doDocAPI(runtime *common.RuntimeContext, method, apiPath string, body interface{}) (map[string]interface{}, error) {
|
|
data, err := runtime.CallAPITyped(method, apiPath, nil, body)
|
|
if err != nil {
|
|
return data, err
|
|
}
|
|
if data == nil {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "document API returned an empty data object")
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func docsAPIOperationFailed(data map[string]interface{}) bool {
|
|
return strings.EqualFold(strings.TrimSpace(common.GetString(data, "result")), "failed")
|
|
}
|
|
|
|
func docsSceneFromContext(ctx context.Context) string {
|
|
if ctx == nil {
|
|
return ""
|
|
}
|
|
scene, _ := ctx.Value(docsSceneContextKey).(string)
|
|
return strings.TrimSpace(scene)
|
|
}
|
|
|
|
func injectDocsScene(runtime *common.RuntimeContext, body map[string]interface{}) {
|
|
if scene := docsSceneFromContext(runtime.Ctx()); scene != "" {
|
|
body["scene"] = scene
|
|
}
|
|
}
|
|
|
|
func buildDriveRouteExtra(docID string) (string, error) {
|
|
extra, err := json.Marshal(map[string]string{"drive_route_token": docID})
|
|
if err != nil {
|
|
return "", errs.NewInternalError(errs.SubtypeUnknown, "failed to marshal upload extra data: %v", err).WithCause(err)
|
|
}
|
|
return string(extra), nil
|
|
}
|
|
|
|
func appendDocWarning(data map[string]interface{}, warning string) {
|
|
if data == nil {
|
|
return
|
|
}
|
|
if strings.TrimSpace(warning) == "" {
|
|
return
|
|
}
|
|
switch existing := data["warnings"].(type) {
|
|
case []interface{}:
|
|
data["warnings"] = append(existing, warning)
|
|
case []string:
|
|
data["warnings"] = append(existing, warning)
|
|
case nil:
|
|
data["warnings"] = []string{warning}
|
|
default:
|
|
data["warnings"] = []interface{}{existing, warning}
|
|
}
|
|
}
|