Files
larksuite-cli/internal/output/jq.go
SunPeiYang996 e42033f5b5 feat(doc): add v2 API for docs +create / +fetch / +update (#638)
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
2026-04-23 23:24:30 +08:00

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
}
}