From c04da4723a0cd7e10a54f9038fc761824bf62f5d Mon Sep 17 00:00:00 2001 From: zhaojunlin0405 Date: Wed, 8 Jul 2026 21:32:27 +0800 Subject: [PATCH] fix: register and consume --json shorthand for custom-format shortcuts (#1737) * fix: decouple --json shorthand registration from default format injection * fix: fold --json shorthand into format flag before consumption * fix: enable --json shorthand for mail +triage and mail +watch * fix: enable --json shorthand for base +record-list * docs: document --json shorthand for triage, watch and record-list * docs: clarify record-list JSON output for script consumption * docs: guide agents to JSON output for machine consumption scenarios * test: make test comments self-contained * test: assert typed error metadata in enum validation test * docs: correct mail +watch --format default and enum in skill doc * docs: keep --json shorthand undocumented as a silent fallback --- shortcuts/base/record_json_shorthand_test.go | 73 +++++++ shortcuts/base/record_list.go | 1 + shortcuts/common/runner.go | 74 ++++++- .../common/runner_json_shorthand_test.go | 200 ++++++++++++++++++ shortcuts/mail/mail_json_shorthand_test.go | 112 ++++++++++ shortcuts/mail/mail_triage.go | 2 +- shortcuts/mail/mail_watch.go | 2 +- .../lark-mail/references/lark-mail-watch.md | 2 +- 8 files changed, 460 insertions(+), 6 deletions(-) create mode 100644 shortcuts/base/record_json_shorthand_test.go create mode 100644 shortcuts/common/runner_json_shorthand_test.go create mode 100644 shortcuts/mail/mail_json_shorthand_test.go diff --git a/shortcuts/base/record_json_shorthand_test.go b/shortcuts/base/record_json_shorthand_test.go new file mode 100644 index 000000000..25aeb4a49 --- /dev/null +++ b/shortcuts/base/record_json_shorthand_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/shortcuts/common" +) + +func mountBaseShortcutFlags(t *testing.T, s common.Shortcut, name string) *cobra.Command { + t.Helper() + parent := &cobra.Command{Use: "test"} + s.Mount(parent, &cmdutil.Factory{}) + cmd, _, err := parent.Find([]string{name}) + if err != nil { + t.Fatalf("Find(%s) error = %v", name, err) + } + return cmd +} + +// record-list 获得 --json 简写 +func TestRecordListRegistersJSONShorthand(t *testing.T) { + cmd := mountBaseShortcutFlags(t, BaseRecordList, "+record-list") + fl := cmd.Flags().Lookup("json") + if fl == nil { + t.Fatal("+record-list missing --json shorthand") + } + if fl.Usage != "shorthand for --format json" { + t.Errorf("usage = %q, want shorthand", fl.Usage) + } + if def := cmd.Flags().Lookup("format").DefValue; def != "markdown" { + t.Errorf("format default = %q, want markdown (unchanged)", def) + } +} + +// record-search / record-get 的 --json 保持请求体语义,不被覆盖(回归锚点) +func TestRecordSearchGetKeepRequestBodyJSON(t *testing.T) { + for _, tc := range []struct { + name string + shortcut common.Shortcut + cmdName string + }{ + {"record-search", BaseRecordSearch, "+record-search"}, + {"record-get", BaseRecordGet, "+record-get"}, + } { + cmd := mountBaseShortcutFlags(t, tc.shortcut, tc.cmdName) + fl := cmd.Flags().Lookup("json") + if fl == nil { + t.Fatalf("%s: --json (request body) missing", tc.name) + } + if strings.Contains(fl.Usage, "shorthand") { + t.Fatalf("%s: request-body --json overwritten by shorthand: %q", tc.name, fl.Usage) + } + if fl.Value.Type() != "string" { + t.Fatalf("%s: --json type = %q, want string", tc.name, fl.Value.Type()) + } + } +} + +// Enum 已接入:help 描述携带枚举后缀(框架对带 Enum 的 flag 自动追加 " (markdown|json)") +func TestRecordReadFormatFlagCarriesEnum(t *testing.T) { + cmd := mountBaseShortcutFlags(t, BaseRecordList, "+record-list") + usage := cmd.Flags().Lookup("format").Usage + if !strings.Contains(usage, "(markdown|json)") { + t.Fatalf("format usage missing enum suffix: %q", usage) + } +} diff --git a/shortcuts/base/record_list.go b/shortcuts/base/record_list.go index b91bb742d..d115768bf 100644 --- a/shortcuts/base/record_list.go +++ b/shortcuts/base/record_list.go @@ -85,6 +85,7 @@ func recordReadFormatFlag() common.Flag { return common.Flag{ Name: "format", Default: "markdown", + Enum: []string{"markdown", "json"}, Desc: "output format: markdown (default) | json", } } diff --git a/shortcuts/common/runner.go b/shortcuts/common/runner.go index 0dc1078e4..28ee04973 100644 --- a/shortcuts/common/runner.go +++ b/shortcuts/common/runner.go @@ -1027,6 +1027,7 @@ func newRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, conf } rctx.larkSDK = sdk + applyJSONShorthand(cmd, s) rctx.Format = rctx.Str("format") rctx.JqExpr, _ = cmd.Flags().GetString("jq") return rctx, nil @@ -1172,6 +1173,75 @@ func registerShortcutFlags(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) registerShortcutFlagsWithContext(context.Background(), cmd, f, s) } +// shortcutDeclaresJSONFlag reports whether the shortcut itself declares a flag +// named "json" in its Flags list (custom semantics, e.g. event +subscribe's +// pretty-print switch or base +record-search's request-body payload). +// Framework-injected flags never appear in s.Flags, so this cleanly separates +// "self-declared json" from "injected shorthand". +func shortcutDeclaresJSONFlag(s *Shortcut) bool { + for _, fl := range s.Flags { + if fl.Name == "json" { + return true + } + } + return false +} + +// shortcutFormatSupportsJSON reports whether the command's format flag accepts +// "json": a self-declared format supports it only when its Enum lists "json"; +// a framework-injected default format (no format entry in s.Flags) always does. +func shortcutFormatSupportsJSON(s *Shortcut) bool { + for _, fl := range s.Flags { + if fl.Name == "format" { + return slices.Contains(fl.Enum, "json") + } + } + return true // framework-injected: json (default) | pretty | table | ndjson | csv +} + +// ensureJSONShorthand registers --json as a shorthand for --format json when: +// 1. the command has a format flag (self-declared or framework-injected), AND +// 2. that format supports "json" (see shortcutFormatSupportsJSON), AND +// 3. no flag named "json" is registered yet — pflag panics on duplicate +// registration, and commands that declare their own --json (event +// +subscribe, base +record-search/-get) keep their custom semantics. +func ensureJSONShorthand(cmd *cobra.Command, s *Shortcut) { + // A shortcut that declares its own "json" flag defines custom semantics + // (e.g. pretty-print switch, request-body payload) — never a shorthand. + if shortcutDeclaresJSONFlag(s) { + return + } + if cmd.Flags().Lookup("format") == nil { + return + } + if !shortcutFormatSupportsJSON(s) { + return + } + // Safety net: pflag panics on duplicate registration. + if cmd.Flags().Lookup("json") != nil { + return + } + cmd.Flags().Bool("json", false, "shorthand for --format json") +} + +// applyJSONShorthand folds the injected --json shorthand into the format flag +// itself, before rctx.Format caches it — so both the cached value (OutFormat, +// ValidateJqFlags, dry-run) and later runtime.Str("format") reads observe +// "json". An explicitly passed --format always wins over the shorthand (the +// shorthand only fills in when the user did not choose a format). Shortcuts +// that declare their own "json" flag keep its custom semantics untouched. +func applyJSONShorthand(cmd *cobra.Command, s *Shortcut) { + if shortcutDeclaresJSONFlag(s) { + return + } + if cmd.Flags().Lookup("json") == nil || cmd.Flags().Changed("format") { + return + } + if set, _ := cmd.Flags().GetBool("json"); set { + _ = cmd.Flags().Set("format", "json") + } +} + func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) { for _, fl := range s.Flags { desc := fl.Desc @@ -1235,10 +1305,8 @@ func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f cmdutil.RegisterFlagCompletion(cmd, "format", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return []string{"json", "pretty", "table", "ndjson", "csv"}, cobra.ShellCompDirectiveNoFileComp }) - if cmd.Flags().Lookup("json") == nil { - cmd.Flags().Bool("json", false, "shorthand for --format json") - } } + ensureJSONShorthand(cmd, s) if s.Risk == "high-risk-write" { cmd.Flags().Bool("yes", false, "confirm high-risk operation") } diff --git a/shortcuts/common/runner_json_shorthand_test.go b/shortcuts/common/runner_json_shorthand_test.go new file mode 100644 index 000000000..a3c01697d --- /dev/null +++ b/shortcuts/common/runner_json_shorthand_test.go @@ -0,0 +1,200 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package common + +import ( + "context" + "testing" + + "github.com/spf13/cobra" + + "github.com/larksuite/cli/internal/cmdutil" +) + +const jsonShorthandUsage = "shorthand for --format json" + +func mountTestShortcut(t *testing.T, s Shortcut) *cobra.Command { + t.Helper() + f, _, _, _ := cmdutil.TestFactory(t, nil) + parent := &cobra.Command{Use: "root"} + s.Mount(parent, f) + cmd, _, err := parent.Find([]string{s.Command}) + if err != nil { + t.Fatalf("Find() error = %v", err) + } + return cmd +} + +// 自定义 format 且 Enum 含 json → 注册简写(本次修复的核心行为) +func TestJSONShorthand_CustomFormatWithJSONEnum_Registered(t *testing.T) { + cmd := mountTestShortcut(t, Shortcut{ + Service: "mail", Command: "+fake-triage", Description: "x", + Flags: []Flag{{Name: "format", Default: "table", Enum: []string{"table", "json", "data"}, Desc: "fmt"}}, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + }) + fl := cmd.Flags().Lookup("json") + if fl == nil { + t.Fatal("--json not registered for custom-format shortcut whose Enum contains json") + } + if fl.Usage != jsonShorthandUsage { + t.Errorf("usage = %q, want %q", fl.Usage, jsonShorthandUsage) + } + // 默认输出格式不被改变 + if def := cmd.Flags().Lookup("format").DefValue; def != "table" { + t.Errorf("format default = %q, want table", def) + } +} + +// 自定义 format 但 Enum 不含 json → 不注册 +func TestJSONShorthand_CustomFormatWithoutJSONEnum_NotRegistered(t *testing.T) { + cmd := mountTestShortcut(t, Shortcut{ + Service: "x", Command: "+no-json", Description: "x", + Flags: []Flag{{Name: "format", Default: "csv", Enum: []string{"csv", "table"}, Desc: "fmt"}}, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + }) + if cmd.Flags().Lookup("json") != nil { + t.Fatal("--json must NOT be registered when format Enum lacks json") + } +} + +// 自定义 format 但无 Enum(现状 triage 形态)→ 不注册(Enum 是判定依据) +func TestJSONShorthand_CustomFormatNoEnum_NotRegistered(t *testing.T) { + cmd := mountTestShortcut(t, Shortcut{ + Service: "x", Command: "+legacy", Description: "x", + Flags: []Flag{{Name: "format", Default: "table", Desc: "fmt"}}, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + }) + if cmd.Flags().Lookup("json") != nil { + t.Fatal("--json must NOT be registered when format has no Enum metadata") + } +} + +// 自声明 json flag(subscribe 的 pretty / record-search 的请求体)→ 不覆盖、不 panic、语义保留 +func TestJSONShorthand_SelfDeclaredJSON_Preserved(t *testing.T) { + cmd := mountTestShortcut(t, Shortcut{ + Service: "event", Command: "+fake-subscribe", Description: "x", + Flags: []Flag{ + {Name: "json", Type: "bool", Desc: "pretty-print JSON instead of NDJSON"}, + }, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + }) + fl := cmd.Flags().Lookup("json") + if fl == nil { + t.Fatal("self-declared --json missing") + } + if fl.Usage != "pretty-print JSON instead of NDJSON" { + t.Errorf("self-declared --json usage overwritten: %q", fl.Usage) + } +} + +// parseMounted mounts the shortcut and parses args against the command's FlagSet +// (registration side effects included), without executing RunE. +func parseMounted(t *testing.T, s Shortcut, args []string) *cobra.Command { + t.Helper() + cmd := mountTestShortcut(t, s) + if err := cmd.ParseFlags(args); err != nil { + t.Fatalf("ParseFlags(%v) error = %v", args, err) + } + return cmd +} + +func customFormatShortcut() Shortcut { + return Shortcut{ + Service: "mail", Command: "+fake-triage", Description: "x", + Flags: []Flag{{Name: "format", Default: "table", Enum: []string{"table", "json", "data"}, Desc: "fmt"}}, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + } +} + +// --json 单独使用 → format 归一化为 json +func TestApplyJSONShorthand_JSONAlone_SetsFormatJSON(t *testing.T) { + s := customFormatShortcut() + cmd := parseMounted(t, s, []string{"--json"}) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "json" { + t.Fatalf("format = %q, want json", got) + } +} + +// 显式 --format 优先于 --json 简写:--format table --json → table +func TestApplyJSONShorthand_ExplicitFormatWins(t *testing.T) { + s := customFormatShortcut() + cmd := parseMounted(t, s, []string{"--format", "table", "--json"}) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "table" { + t.Fatalf("format = %q, want table (explicit --format must win)", got) + } +} + +// --format json --json → json(一致,无冲突) +func TestApplyJSONShorthand_ExplicitJSONFormatConsistent(t *testing.T) { + s := customFormatShortcut() + cmd := parseMounted(t, s, []string{"--format", "json", "--json"}) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "json" { + t.Fatalf("format = %q, want json", got) + } +} + +// 均不传 → 默认值不变 +func TestApplyJSONShorthand_NoFlags_DefaultUntouched(t *testing.T) { + s := customFormatShortcut() + cmd := parseMounted(t, s, nil) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "table" { + t.Fatalf("format = %q, want table (default untouched)", got) + } +} + +// 自声明 string 型 --json(record-search 形态:format+json 双声明)→ 归一化跳过 +func TestApplyJSONShorthand_SelfDeclaredStringJSON_Skipped(t *testing.T) { + s := Shortcut{ + Service: "base", Command: "+fake-record-search", Description: "x", + Flags: []Flag{ + {Name: "format", Default: "markdown", Enum: []string{"markdown", "json"}, Desc: "fmt"}, + {Name: "json", Desc: "request body JSON object"}, + }, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + } + cmd := parseMounted(t, s, []string{"--json", `{"keyword":"Alice"}`}) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "markdown" { + t.Fatalf("format = %q, want markdown (self-declared json must not normalize)", got) + } + if got := cmd.Flags().Lookup("json").Value.String(); got != `{"keyword":"Alice"}` { + t.Fatalf("request-body --json corrupted: %q", got) + } +} + +// 自声明 bool 型 --json(subscribe 形态:无自定义 format,框架注入 format)→ 归一化跳过 +func TestApplyJSONShorthand_SelfDeclaredBoolJSON_Skipped(t *testing.T) { + s := Shortcut{ + Service: "event", Command: "+fake-subscribe", Description: "x", + Flags: []Flag{ + {Name: "json", Type: "bool", Desc: "pretty-print JSON instead of NDJSON"}, + }, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + } + cmd := parseMounted(t, s, []string{"--json"}) + applyJSONShorthand(cmd, &s) + // 注入的 format 默认即 json;这里断言的是 Changed 状态未被归一化污染 + if cmd.Flags().Changed("format") { + t.Fatal("normalization must not touch format for shortcuts declaring their own --json") + } +} + +// 无自定义 format(普通命令)→ 注入默认 format + 简写(现状回归) +func TestJSONShorthand_DefaultInjectedFormat_StillRegistered(t *testing.T) { + cmd := mountTestShortcut(t, Shortcut{ + Service: "im", Command: "+plain", Description: "x", + Execute: func(context.Context, *RuntimeContext) error { return nil }, + }) + fl := cmd.Flags().Lookup("json") + if fl == nil { + t.Fatal("--json missing on default-format shortcut (regression)") + } + if fl.Usage != jsonShorthandUsage { + t.Errorf("usage = %q, want %q", fl.Usage, jsonShorthandUsage) + } +} diff --git a/shortcuts/mail/mail_json_shorthand_test.go b/shortcuts/mail/mail_json_shorthand_test.go new file mode 100644 index 000000000..dec3f5d20 --- /dev/null +++ b/shortcuts/mail/mail_json_shorthand_test.go @@ -0,0 +1,112 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package mail + +import ( + "errors" + "strings" + "testing" + + "github.com/larksuite/cli/errs" +) + +// help 必须列出 --json 简写 +func TestMailTriageHelpListsJSONShorthand(t *testing.T) { + f, stdout, _, _ := mailShortcutTestFactory(t) + if err := runMountedMailShortcutWithCobraOutput(t, MailTriage, []string{"+triage", "-h"}, f, stdout); err != nil { + t.Fatalf("help returned error: %v", err) + } + if !strings.Contains(stdout.String(), "shorthand for --format json") { + t.Fatalf("triage help missing --json shorthand\n%s", stdout.String()) + } +} + +func TestMailWatchHelpListsJSONShorthand(t *testing.T) { + f, stdout, _, _ := mailShortcutTestFactory(t) + if err := runMountedMailShortcutWithCobraOutput(t, MailWatch, []string{"+watch", "-h"}, f, stdout); err != nil { + t.Fatalf("help returned error: %v", err) + } + if !strings.Contains(stdout.String(), "shorthand for --format json") { + t.Fatalf("watch help missing --json shorthand\n%s", stdout.String()) + } +} + +// 行为验证:--json 走 JSON 输出路径,不输出 table read hint +func TestMailTriageJSONShorthandDoesNotEmitReadHint(t *testing.T) { + f, stdout, stderr, reg := mailShortcutTestFactory(t) + registerTriageReadHintStubs(reg) + + err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--json", "--max", "1"}, f, stdout) + if err != nil { + t.Fatalf("triage --json returned error: %v", err) + } + reg.Verify(t) + if strings.Contains(stderr.String(), "tip: read full content:") { + t.Fatalf("--json must follow the JSON path, got table hint\nstderr=%s", stderr.String()) + } + if !strings.Contains(stdout.String(), `"messages"`) { + t.Fatalf("--json stdout missing JSON payload\n%s", stdout.String()) + } +} + +// 等价性验证:--json 与 --format json 的 dry-run 输出一致 +func TestMailTriageJSONShorthandDryRunEquivalence(t *testing.T) { + f1, stdout1, _, _ := mailShortcutTestFactory(t) + if err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--json", "--max", "1", "--dry-run"}, f1, stdout1); err != nil { + t.Fatalf("--json --dry-run error: %v", err) + } + f2, stdout2, _, _ := mailShortcutTestFactory(t) + if err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--format", "json", "--max", "1", "--dry-run"}, f2, stdout2); err != nil { + t.Fatalf("--format json --dry-run error: %v", err) + } + if stdout1.String() != stdout2.String() { + t.Fatalf("dry-run outputs differ:\n--json:\n%s\n--format json:\n%s", stdout1.String(), stdout2.String()) + } +} + +// 优先级验证:显式 --format table 优先,--json 让位 → 仍走 table 路径 +func TestMailTriageExplicitTableWinsOverJSONShorthand(t *testing.T) { + f, stdout, stderr, reg := mailShortcutTestFactory(t) + registerTriageReadHintStubs(reg) + + err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--format", "table", "--json", "--max", "1"}, f, stdout) + if err != nil { + t.Fatalf("triage returned error: %v", err) + } + if !strings.Contains(stderr.String(), "tip: read full content:") { + t.Fatalf("explicit --format table must win over --json (expected table hint)\nstderr=%s", stderr.String()) + } +} + +// 错误验证:Enum 硬校验 +func TestMailTriageEnumRejectsUnknownFormat(t *testing.T) { + f, stdout, _, _ := mailShortcutTestFactory(t) + err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--format", "bogus", "--max", "1", "--dry-run"}, f, stdout) + if err == nil { + t.Fatal("expected validation error for --format bogus") + } + problem, ok := errs.ProblemOf(err) + if !ok { + t.Fatalf("error = %T, want typed errs problem carrier", err) + } + if problem.Category != errs.CategoryValidation { + t.Fatalf("category = %q, want %q", problem.Category, errs.CategoryValidation) + } + if problem.Subtype != errs.SubtypeInvalidArgument { + t.Fatalf("subtype = %q, want %q", problem.Subtype, errs.SubtypeInvalidArgument) + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("error = %T, want *errs.ValidationError", err) + } + if ve.Param != "--format" { + t.Fatalf("param = %q, want --format", ve.Param) + } + if !strings.Contains(problem.Message, `invalid value "bogus" for --format`) { + t.Fatalf("message = %q, want enum validation message", problem.Message) + } + if !strings.Contains(problem.Message, "table, json, data") { + t.Fatalf("message = %q, want allowed values list", problem.Message) + } +} diff --git a/shortcuts/mail/mail_triage.go b/shortcuts/mail/mail_triage.go index a4048ff15..974d65662 100644 --- a/shortcuts/mail/mail_triage.go +++ b/shortcuts/mail/mail_triage.go @@ -55,7 +55,7 @@ var MailTriage = common.Shortcut{ Scopes: []string{"mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ - {Name: "format", Default: "table", Desc: "output format: table | json | data (json/data output object with pagination fields)"}, + {Name: "format", Default: "table", Enum: []string{"table", "json", "data"}, Desc: "output format: table | json | data (json/data output object with pagination fields)"}, {Name: "max", Type: "int", Default: "20", Desc: "maximum number of messages to fetch (1-400; auto-paginates internally)"}, {Name: "page-size", Type: "int", Desc: "alias for --max"}, {Name: "page-token", Desc: "pagination token from a previous response to fetch the next page"}, diff --git a/shortcuts/mail/mail_watch.go b/shortcuts/mail/mail_watch.go index e7560845e..ff877313c 100644 --- a/shortcuts/mail/mail_watch.go +++ b/shortcuts/mail/mail_watch.go @@ -99,7 +99,7 @@ var MailWatch = common.Shortcut{ Scopes: []string{"mail:event", "mail:user_mailbox.event.mail_address:read", "mail:user_mailbox:readonly", "mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, AuthTypes: []string{"user"}, Flags: []common.Flag{ - {Name: "format", Default: "data", Desc: "json: NDJSON stream with ok/data envelope; data: bare NDJSON stream"}, + {Name: "format", Default: "data", Enum: []string{"json", "data"}, Desc: "json: NDJSON stream with ok/data envelope; data: bare NDJSON stream"}, {Name: "msg-format", Default: "metadata", Desc: "message payload mode: metadata(headers + meta, for triage/notification) | minimal(IDs and state only, no headers, for tracking read/folder changes) | plain_text_full(all metadata fields + full plain-text body) | event(raw WebSocket event, no API call, for debug) | full(full message including HTML body and attachments)"}, {Name: "output-dir", Desc: "Write each message as a JSON file (always full payload, regardless of --msg-format)"}, {Name: "mailbox", Default: "me", Desc: "email address (default: me)"}, diff --git a/skills/lark-mail/references/lark-mail-watch.md b/skills/lark-mail/references/lark-mail-watch.md index 43c5fd8d8..78d19501d 100644 --- a/skills/lark-mail/references/lark-mail-watch.md +++ b/skills/lark-mail/references/lark-mail-watch.md @@ -47,7 +47,7 @@ lark-cli mail +watch --print-output-schema |------|------|------| | `--mailbox ` | `me` | 订阅目标邮箱 | | `--msg-format ` | `metadata` | 输出模式:`metadata` / `minimal` / `plain_text_full` / `full` / `event` | -| `--format ` | `table` | 输出样式:`table` / `json` / `data` | +| `--format ` | `data` | 输出样式:`json`(带 ok/data 信封的 NDJSON 流)/ `data`(裸 NDJSON 流) | | `--folder-ids ` | — | 文件夹 ID 过滤,如 `["INBOX","SENT"]` | | `--folders ` | — | 文件夹名称过滤(与 `--folder-ids` 取并集) | | `--label-ids ` | — | 标签 ID 过滤,如 `["FLAGGED","IMPORTANT"]` |