mirror of
https://github.com/larksuite/cli.git
synced 2026-07-04 06:29:52 +08:00
Adds an `--api-version v2` path to the docs shortcuts, backed by the
`docs_ai/v1/documents` OpenAPI. DocxXML is the default document format
and Markdown is available as an alternative. Content input is unified
across the three shortcuts via `--content` + `--doc-format`. The v1
(MCP) path is preserved for backward compatibility and now prints a
deprecation notice on use.
Shortcuts:
- `docs +create --api-version v2`: create a document from XML or
Markdown, with optional `--parent-token` or `--parent-position`.
Bot identity continues to auto-grant the current CLI user
full_access on the new document.
- `docs +fetch --api-version v2`: adds `--detail simple|with-ids|full`
for export granularity and `--scope full|outline|range|keyword|section`
for partial reads, along with `--context-before` / `--context-after`,
`--max-depth`, and `--revision-id`.
- `docs +update --api-version v2`: introduces structured operations
via `--command`: `str_replace`, `block_delete`, `block_insert_after`,
`block_copy_insert_after`, `block_replace`, `block_move_after`,
`overwrite`, `append`.
Framework support in `shortcuts/common`:
- `OutRaw` / `OutFormatRaw` emit the JSON envelope with HTML escaping
disabled so XML/HTML document bodies are preserved verbatim.
- New `Shortcut.PostMount` hook runs after a cobra.Command is fully
configured; used here to install a version-aware help function
that hides flags belonging to the inactive `--api-version`.
Also refreshes the lark-doc skill pack (SKILL.md, create/fetch/update
references, new lark-doc-xml and lark-doc-md references, style and
workflow guides), README examples, and downstream skill call sites
(lark-drive, lark-vc, lark-whiteboard, lark-workflow-meeting-summary,
lark-event).
Change-Id: Ide2d86b190a4e21095ae29096e7fb00031d80489
157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package output
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
|
|
"github.com/itchyny/gojq"
|
|
)
|
|
|
|
// JqFilter applies a jq expression to data and writes the results to w.
|
|
// Scalar values are printed raw (no quotes for strings), matching jq -r behavior.
|
|
// Complex values (maps, arrays) are printed as indented JSON with Go's default
|
|
// HTML escaping (<, >, & → <, >, &).
|
|
func JqFilter(w io.Writer, data interface{}, expr string) error {
|
|
return jqFilter(w, data, expr, false)
|
|
}
|
|
|
|
// JqFilterRaw is like JqFilter but disables HTML escaping when re-marshaling
|
|
// complex jq results. Use it alongside OutRaw when the upstream envelope
|
|
// carries XML/HTML content that must survive --jq '.data.document' style
|
|
// projections without getting mangled into < escapes.
|
|
func JqFilterRaw(w io.Writer, data interface{}, expr string) error {
|
|
return jqFilter(w, data, expr, true)
|
|
}
|
|
|
|
func jqFilter(w io.Writer, data interface{}, expr string, raw bool) error {
|
|
query, err := gojq.Parse(expr)
|
|
if err != nil {
|
|
return ErrValidation("invalid jq expression: %s", err)
|
|
}
|
|
code, err := gojq.Compile(query)
|
|
if err != nil {
|
|
return ErrValidation("invalid jq expression: %s", err)
|
|
}
|
|
|
|
// Normalize data through toGeneric so typed structs become map[string]any.
|
|
normalized := toGeneric(data)
|
|
// Convert json.Number values to gojq-compatible types.
|
|
normalized = convertNumbers(normalized)
|
|
|
|
iter := code.Run(normalized)
|
|
for {
|
|
v, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if err, isErr := v.(error); isErr {
|
|
return Errorf(ExitAPI, "jq_error", "jq error: %s", err)
|
|
}
|
|
if err := writeJqValue(w, v, raw); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateJqFlags checks --jq flag compatibility with --output and --format flags,
|
|
// and validates the jq expression syntax. Returns nil if jqExpr is empty.
|
|
func ValidateJqFlags(jqExpr, outputFlag, format string) error {
|
|
if jqExpr == "" {
|
|
return nil
|
|
}
|
|
if outputFlag != "" {
|
|
return ErrValidation("--jq and --output are mutually exclusive")
|
|
}
|
|
if format != "" && format != "json" {
|
|
return ErrValidation("--jq and --format %s are mutually exclusive", format)
|
|
}
|
|
return ValidateJqExpression(jqExpr)
|
|
}
|
|
|
|
// ValidateJqExpression checks whether a jq expression is syntactically valid.
|
|
func ValidateJqExpression(expr string) error {
|
|
query, err := gojq.Parse(expr)
|
|
if err != nil {
|
|
return ErrValidation("invalid jq expression: %s", err)
|
|
}
|
|
_, err = gojq.Compile(query)
|
|
if err != nil {
|
|
return ErrValidation("invalid jq expression: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// writeJqValue writes a single jq result value to w.
|
|
// Scalars are printed raw; complex values as indented JSON.
|
|
// When raw is true, HTML escaping is disabled on complex values so that
|
|
// embedded XML/HTML content is preserved as-is.
|
|
func writeJqValue(w io.Writer, v interface{}, raw bool) error {
|
|
switch val := v.(type) {
|
|
case nil:
|
|
fmt.Fprintln(w, "null")
|
|
case bool:
|
|
fmt.Fprintln(w, val)
|
|
case int:
|
|
fmt.Fprintln(w, val)
|
|
case float64:
|
|
// Use %g to avoid trailing zeros, matching jq behavior.
|
|
fmt.Fprintf(w, "%g\n", val)
|
|
case *big.Int:
|
|
fmt.Fprintln(w, val.String())
|
|
case string:
|
|
// Raw output for strings (no quotes), matching jq -r.
|
|
fmt.Fprintln(w, val)
|
|
default:
|
|
// Complex value (map, array): indented JSON.
|
|
if raw {
|
|
enc := json.NewEncoder(w)
|
|
enc.SetEscapeHTML(false)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(v); err != nil {
|
|
return Errorf(ExitInternal, "jq_error", "failed to marshal jq result: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
b, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
return Errorf(ExitInternal, "jq_error", "failed to marshal jq result: %s", err)
|
|
}
|
|
fmt.Fprintln(w, string(b))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// convertNumbers recursively converts json.Number values to int or float64
|
|
// so that gojq can process them correctly.
|
|
func convertNumbers(v interface{}) interface{} {
|
|
switch val := v.(type) {
|
|
case json.Number:
|
|
if i, err := val.Int64(); err == nil {
|
|
return int(i)
|
|
}
|
|
if f, err := val.Float64(); err == nil {
|
|
return f
|
|
}
|
|
// Fallback: return as string (shouldn't happen for valid JSON numbers).
|
|
return val.String()
|
|
case map[string]interface{}:
|
|
for k, elem := range val {
|
|
val[k] = convertNumbers(elem)
|
|
}
|
|
return val
|
|
case []interface{}:
|
|
for i, elem := range val {
|
|
val[i] = convertNumbers(elem)
|
|
}
|
|
return val
|
|
default:
|
|
return v
|
|
}
|
|
}
|