mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 22:24:31 +08:00
* feat(base): support batch record get and delete * fix(base): address batch record PR feedback * docs(base): refine record skill routing * refactor(base): use batch record get and delete only * refactor(base): share record selection normalization * docs(base): clarify record get field projection help
338 lines
9.1 KiB
Go
338 lines
9.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package base
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
const maxRecordMarkdownIgnoredFields = 20
|
|
|
|
func validateRecordReadFormat(runtime *common.RuntimeContext) error {
|
|
switch runtime.Str("format") {
|
|
case "", "json", "markdown":
|
|
return nil
|
|
default:
|
|
return output.ErrValidation("--format must be json or markdown")
|
|
}
|
|
}
|
|
|
|
func outputRecordMarkdown(runtime *common.RuntimeContext, data map[string]interface{}) error {
|
|
return outputRecordMarkdownWithRenderer(runtime, data, renderRecordMarkdown)
|
|
}
|
|
|
|
func outputRecordMarkdownWithRenderer(runtime *common.RuntimeContext, data map[string]interface{}, renderer func(map[string]interface{}) (string, error)) error {
|
|
if runtime.JqExpr != "" {
|
|
if !runtime.Changed("format") {
|
|
runtime.Out(data, nil)
|
|
return nil
|
|
}
|
|
return output.ErrValidation("--jq and --format markdown are mutually exclusive")
|
|
}
|
|
rendered, err := renderer(data)
|
|
if err != nil {
|
|
fmt.Fprintf(runtime.IO().ErrOut, "warning: record markdown render failed, falling back to json: %v\n", err)
|
|
runtime.Out(data, nil)
|
|
return nil
|
|
}
|
|
scanResult := output.ScanForSafety(runtime.Cmd.CommandPath(), data, runtime.IO().ErrOut)
|
|
if scanResult.Blocked {
|
|
return scanResult.BlockErr
|
|
}
|
|
if scanResult.Alert != nil {
|
|
output.WriteAlertWarning(runtime.IO().ErrOut, scanResult.Alert)
|
|
}
|
|
fmt.Fprint(runtime.IO().Out, rendered)
|
|
return nil
|
|
}
|
|
|
|
func outputRecordGetMarkdown(runtime *common.RuntimeContext, data map[string]interface{}) error {
|
|
return outputRecordMarkdownWithRenderer(runtime, data, renderRecordGetMarkdown)
|
|
}
|
|
|
|
func renderRecordGetMarkdown(data map[string]interface{}) (string, error) {
|
|
fields := stringSliceValue(data["fields"])
|
|
recordIDs := stringSliceValue(data["record_id_list"])
|
|
rows, ok := data["data"].([]interface{})
|
|
if len(fields) == 0 || !ok {
|
|
return "", output.ErrValidation("--format markdown requires record matrix response with fields, record_id_list, and data")
|
|
}
|
|
if len(recordIDs) == 1 && len(rows) == 1 {
|
|
rowItems, _ := rows[0].([]interface{})
|
|
if recordMarkedNotFound(data["record_not_found"], recordIDs[0]) {
|
|
return renderMissingSingleRecordMarkdown(recordIDs[0], data), nil
|
|
}
|
|
return renderSingleRecordMarkdown(recordIDs[0], fields, rowItems, data), nil
|
|
}
|
|
return renderRecordMarkdown(data)
|
|
}
|
|
|
|
func renderRecordMarkdown(data map[string]interface{}) (string, error) {
|
|
fields := stringSliceValue(data["fields"])
|
|
recordIDs := stringSliceValue(data["record_id_list"])
|
|
rows, ok := data["data"].([]interface{})
|
|
if len(fields) == 0 || !ok {
|
|
return "", output.ErrValidation("--format markdown requires record matrix response with fields, record_id_list, and data")
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("`_record_id` is metadata for record operations, not a table field.\n\n")
|
|
|
|
columns := append([]string{"_record_id"}, fields...)
|
|
writeMarkdownRow(&b, columns)
|
|
writeMarkdownSeparator(&b, len(columns))
|
|
for i, rowValue := range rows {
|
|
rowItems, _ := rowValue.([]interface{})
|
|
cells := make([]string, 0, len(columns))
|
|
if i < len(recordIDs) {
|
|
cells = append(cells, recordIDs[i])
|
|
} else {
|
|
cells = append(cells, "")
|
|
}
|
|
for j := range fields {
|
|
if j < len(rowItems) {
|
|
cells = append(cells, markdownCell(rowItems[j]))
|
|
} else {
|
|
cells = append(cells, "")
|
|
}
|
|
}
|
|
writeMarkdownRow(&b, cells)
|
|
}
|
|
|
|
meta := recordMarkdownMeta(data)
|
|
if len(meta) > 0 {
|
|
b.WriteString("\nMeta: ")
|
|
b.WriteString(strings.Join(meta, "; "))
|
|
b.WriteByte('\n')
|
|
}
|
|
if ignored := ignoredFieldsMarkdown(data["ignored_fields"]); ignored != "" {
|
|
b.WriteString("Ignored fields: ")
|
|
b.WriteString(ignored)
|
|
b.WriteByte('\n')
|
|
}
|
|
if missing := recordNotFoundMarkdown(data["record_not_found"]); missing != "" {
|
|
b.WriteString("Missing records: ")
|
|
b.WriteString(missing)
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String(), nil
|
|
}
|
|
|
|
func renderSingleRecordMarkdown(recordID string, fields []string, rowItems []interface{}, data map[string]interface{}) string {
|
|
var b strings.Builder
|
|
b.WriteString("`_record_id` is metadata for record operations, not a table field.\n\n")
|
|
b.WriteString("- `_record_id`: ")
|
|
b.WriteString(markdownInlineValue(recordID))
|
|
b.WriteByte('\n')
|
|
for i, field := range fields {
|
|
b.WriteString("- `")
|
|
b.WriteString(field)
|
|
b.WriteString("`: ")
|
|
if i < len(rowItems) {
|
|
b.WriteString(markdownInlineValue(rowItems[i]))
|
|
}
|
|
b.WriteByte('\n')
|
|
}
|
|
meta := recordMarkdownMeta(data)
|
|
if len(meta) > 0 {
|
|
b.WriteString("\nMeta: ")
|
|
b.WriteString(strings.Join(meta, "; "))
|
|
b.WriteByte('\n')
|
|
}
|
|
if ignored := ignoredFieldsMarkdown(data["ignored_fields"]); ignored != "" {
|
|
b.WriteString("Ignored fields: ")
|
|
b.WriteString(ignored)
|
|
b.WriteByte('\n')
|
|
}
|
|
if missing := recordNotFoundMarkdown(data["record_not_found"]); missing != "" {
|
|
b.WriteString("Missing records: ")
|
|
b.WriteString(missing)
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func renderMissingSingleRecordMarkdown(recordID string, data map[string]interface{}) string {
|
|
var b strings.Builder
|
|
b.WriteString("Record not found.\n\n")
|
|
b.WriteString("- `_record_id`: ")
|
|
b.WriteString(markdownInlineValue(recordID))
|
|
b.WriteByte('\n')
|
|
meta := recordMarkdownMeta(data)
|
|
if len(meta) > 0 {
|
|
b.WriteString("\nMeta: ")
|
|
b.WriteString(strings.Join(meta, "; "))
|
|
b.WriteByte('\n')
|
|
}
|
|
if missing := recordNotFoundMarkdown(data["record_not_found"]); missing != "" {
|
|
b.WriteString("Missing records: ")
|
|
b.WriteString(missing)
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func recordMarkdownMeta(data map[string]interface{}) []string {
|
|
meta := []string{fmt.Sprintf("count=%d", ignoredFieldsCount(data["record_id_list"]))}
|
|
if hasMore, ok := data["has_more"]; ok {
|
|
meta = append(meta, "has_more="+markdownInlineValue(hasMore))
|
|
}
|
|
if queryContext, ok := data["query_context"].(map[string]interface{}); ok {
|
|
for _, key := range []string{"record_scope", "field_scope", "search_scope"} {
|
|
if value, ok := queryContext[key]; ok {
|
|
meta = append(meta, key+"="+markdownInlineValue(value))
|
|
}
|
|
}
|
|
}
|
|
if ignoredCount := ignoredFieldsCount(data["ignored_fields"]); ignoredCount > 0 {
|
|
meta = append(meta, fmt.Sprintf("ignored_fields=%d", ignoredCount))
|
|
}
|
|
if missingCount := ignoredFieldsCount(data["record_not_found"]); missingCount > 0 {
|
|
meta = append(meta, fmt.Sprintf("record_not_found=%d", missingCount))
|
|
}
|
|
return meta
|
|
}
|
|
|
|
func ignoredFieldsCount(value interface{}) int {
|
|
switch v := value.(type) {
|
|
case []interface{}:
|
|
return len(v)
|
|
case []string:
|
|
return len(v)
|
|
case nil:
|
|
return 0
|
|
default:
|
|
return 1
|
|
}
|
|
}
|
|
|
|
func ignoredFieldsMarkdown(value interface{}) string {
|
|
items := markdownListItems(value)
|
|
if len(items) == 0 {
|
|
return ""
|
|
}
|
|
total := len(items)
|
|
if len(items) > maxRecordMarkdownIgnoredFields {
|
|
items = items[:maxRecordMarkdownIgnoredFields]
|
|
items = append(items, fmt.Sprintf("...(%d total)", total))
|
|
}
|
|
return strings.Join(items, ", ")
|
|
}
|
|
|
|
func recordNotFoundMarkdown(value interface{}) string {
|
|
return strings.Join(markdownListItems(value), ", ")
|
|
}
|
|
|
|
func recordMarkedNotFound(value interface{}, recordID string) bool {
|
|
for _, item := range markdownListItems(value) {
|
|
if item == recordID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func markdownListItems(value interface{}) []string {
|
|
switch v := value.(type) {
|
|
case []interface{}:
|
|
items := make([]string, 0, len(v))
|
|
for _, item := range v {
|
|
items = append(items, markdownInlineValue(item))
|
|
}
|
|
return items
|
|
case []string:
|
|
items := make([]string, 0, len(v))
|
|
for _, item := range v {
|
|
items = append(items, markdownInlineValue(item))
|
|
}
|
|
return items
|
|
case nil:
|
|
return nil
|
|
default:
|
|
return []string{markdownInlineValue(v)}
|
|
}
|
|
}
|
|
|
|
func writeMarkdownRow(b *strings.Builder, cells []string) {
|
|
b.WriteString("| ")
|
|
for i, cell := range cells {
|
|
if i > 0 {
|
|
b.WriteString(" | ")
|
|
}
|
|
b.WriteString(markdownTableText(cell))
|
|
}
|
|
b.WriteString(" |\n")
|
|
}
|
|
|
|
func writeMarkdownSeparator(b *strings.Builder, columns int) {
|
|
b.WriteString("| ")
|
|
for i := 0; i < columns; i++ {
|
|
if i > 0 {
|
|
b.WriteString(" | ")
|
|
}
|
|
b.WriteString("---")
|
|
}
|
|
b.WriteString(" |\n")
|
|
}
|
|
|
|
func markdownCell(value interface{}) string {
|
|
return markdownInlineValue(value)
|
|
}
|
|
|
|
func markdownInlineValue(value interface{}) string {
|
|
switch v := value.(type) {
|
|
case nil:
|
|
return ""
|
|
case string:
|
|
return v
|
|
case json.Number:
|
|
return v.String()
|
|
case bool:
|
|
if v {
|
|
return "true"
|
|
}
|
|
return "false"
|
|
case float64:
|
|
return fmt.Sprintf("%v", v)
|
|
case int:
|
|
return fmt.Sprintf("%d", v)
|
|
default:
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
return string(b)
|
|
}
|
|
}
|
|
|
|
func markdownTableText(value string) string {
|
|
value = strings.ReplaceAll(value, "\\", "\\\\")
|
|
value = strings.ReplaceAll(value, "|", "\\|")
|
|
value = strings.ReplaceAll(value, "\r\n", "<br>")
|
|
value = strings.ReplaceAll(value, "\n", "<br>")
|
|
return value
|
|
}
|
|
|
|
func stringSliceValue(value interface{}) []string {
|
|
switch v := value.(type) {
|
|
case []interface{}:
|
|
out := make([]string, 0, len(v))
|
|
for _, item := range v {
|
|
if s, ok := item.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
case []string:
|
|
return append([]string(nil), v...)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|