Skip to content

feat(mail): support triage filter shorthand - #2025

Open
bubbmon233 wants to merge 1 commit into
larksuite:mainfrom
bubbmon233:feat/dc70da0
Open

bubbmon233 wants to merge 1 commit into
larksuite:mainfrom
bubbmon233:feat/dc70da0

Conversation

@bubbmon233

@bubbmon233 bubbmon233 commented Jul 23, 2026 •

Copy link
Copy Markdown
Collaborator

Adds shorthand parsing for mail triage filters and a separate folder flag, while keeping the existing JSON filter flow.

  • Accepts folder_id/folder shorthand and read/unread aliases for triage filters.
  • Merges --folder with --filter and reports conflicts before API calls.
  • Routes read-state filters through the search path when needed.
  • Adds regression tests and updates mail triage usage examples.

Summary by CodeRabbit

  • New Features

    • Added shorthand filter support for mail +triage, including unread, read, folder, and folder ID filters.
    • Added --folder support that combines with compatible filters and provides clearer conflict validation.
    • Updated pagination hints and filter schema guidance.
  • Bug Fixes

    • Corrected routing for read-message searches, including is_unread=false.
  • Documentation

    • Updated triage usage examples and filter descriptions to reflect shorthand and folder options.

@github-actions github-actions Bot added domain/mail PR touches the mail domain size/M Single-domain feat or fix with limited business impact labels Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

mail +triage now supports shorthand filters, merges --folder into runtime filtering, rejects folder conflicts, and routes is_unread=false through search. Tests and documentation cover the new syntax, routing, pagination hints, and schema descriptions.

Changes

Mail triage filter handling

Layer / File(s) Summary
Filter parsing and request routing
shortcuts/mail/mail_triage.go
Runtime filters now accept JSON or shorthand, merge --folder, reject conflicting folder fields, update pagination hints, and route explicit is_unread=false filters to search.
Filter and path regression coverage
shortcuts/mail/mail_triage_test.go
Tests cover shorthand aliases, invalid inputs, folder conflicts, dry-run list/search requests, and search-path resolution.
Triage filter usage documentation
skills/lark-mail/SKILL.md, skills/lark-mail/references/lark-mail-triage.md
Documentation now describes shorthand syntax, --folder, folder conflicts, and is_read search behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant triageFilterFromRuntime
  participant MailAPI
  CLI->>triageFilterFromRuntime: Parse --filter and merge --folder
  triageFilterFromRuntime->>CLI: Return validated triage filter
  CLI->>MailAPI: Request list or search path
  MailAPI->>CLI: Return triage results
Loading

Possibly related PRs

  • larksuite/cli#301: Updates related mail triage routing and page-token path selection.

Suggested labels: feature

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change, but it omits the required Summary/Changes/Test Plan/Related Issues structure. Add the required sections with clear Summary, Changes, Test Plan, and Related Issues entries, including how the change was verified.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding shorthand support for mail triage filters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shortcuts/mail/mail_triage.go`:
- Around line 442-454: The JSON parsing flow around the decoder in the triage
filter parser must require exactly one non-null JSON object. Validate that
decoding produces an object rather than null or another JSON type, then perform
a second decode to confirm EOF and reject trailing values; return a typed
--filter validation error for either condition while preserving existing
unknown-field hints and shorthand fallback for initial JSON decode failures.

In `@skills/lark-mail/references/lark-mail-triage.md`:
- Line 52: Update the option description row near the --filter entry so the
angle-bracket placeholder does not contain an unescaped pipe; replace it with a
pipe-free equivalent such as json-or-shorthand, preserving the documented
support for both formats.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c78efc7-823f-4f9e-a1d8-418a1b3fd4d9

📥 Commits

Reviewing files that changed from the base of the PR and between 67015ee and 315d58c.

📒 Files selected for processing (4)
  • shortcuts/mail/mail_triage.go
  • shortcuts/mail/mail_triage_test.go
  • skills/lark-mail/SKILL.md
  • skills/lark-mail/references/lark-mail-triage.md

Comment on lines +442 to 454
dec := json.NewDecoder(strings.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&filter); err != nil {
if hint := triageFilterUnknownFieldHint(err.Error()); hint != "" {
return triageFilter{}, mailValidationParamError("--filter", "invalid --filter: %s", hint)
}
return triageFilter{}, mailValidationParamError("--filter", "invalid --filter: %s", err)
shorthand, shorthandErr := parseTriageFilterShorthand(raw)
if shorthandErr == nil {
return shorthand, nil
}
return triageFilter{}, shorthandErr
}
return filter, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '436,506p' shortcuts/mail/mail_triage.go
rg -n -C2 'trailing|exactly one JSON|null' shortcuts/mail/mail_triage_test.go || true

Repository: larksuite/cli

Length of output: 2547


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '## parseTriageFilter and filter struct definitions/usages'
ast-grep outline shortcuts/mail/mail_triage.go --match parseTriageFilter --view expanded || true
rg -n --glob '*.go' 'type triageFilter|func \*triageFilter|triageFilter\{' shortcuts/mail || true

echo '## focused tests around parseTriageFilter'
rg -n --glob '*test.go' 'parseTriageFilter|Filter|folder|unread|unknown|trailing|null' shortcuts || true

echo '## Go json.Decoder behavior probe via local source reflection (read-only)'
if command -v go >/dev/null 2>&1; then
  tmp="$(mktemp -d)"
  cat > "$tmp/main.go" <<'GO'
package main

import (
	"encoding/json"
	"fmt"
	"strings"
)

type T struct{ Folder string `json:"folder"` }

func main() {
	for _, raw := range []string{`{"folder":"INBOX"} {"is_unread":true}`, `null`, `{}`, `{"folder":"INBOX"}`} {
		var t T
		c, _ := json.NewDecoder(strings.NewReader(raw)).Decode(&t)
		fmt.Printf("raw=%q decode=%v result=%+v\n", raw, c, t)
	}
}
GO
  (cd "$tmp" && go run main.go)
else
  echo 'go not available'
fi

Repository: larksuite/cli

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '## triageFilter struct and parser call sites'
sed -n '1,60p' shortcuts/mail/mail_triage.go
sed -n '425,456p' shortcuts/mail/mail_triage.go

echo '## parseTriageFilter tests around shorthand/table inputs'
sed -n '217,250p' shortcuts/mail/mail_triage_test.go
sed -n '267,285p' shortcuts/mail/mail_triage_test.go

echo '## behavioral Go probe if available'
if command -v go >/dev/null 2>&1; then
  tmp="$(mktemp -d)"
  cat > "$tmp/main.go" <<'GO'
package main
import (
	"bytes"
	"encoding/json"
	"fmt"
	"strings"
)
type T struct { Folder string `json:"folder"` }
func main() {
	s := map[string]string{
		"concatenated":     `{"folder":"INBOX"} {"is_unread":true}`,
		"concatenated_nl":  `{"folder":"INBOX"}\n {"is_unread":true}`,
		"after_json_object": `{}`,
		"null":              `null`,
		"array":             `["INBOX"]`,
		"plain_object":      `{"folder":"INBOX"}`,
		"plain_object_eof":  `{"folder":"INBOX"}`,
	}
	for name, raw := range s {
		data := []byte(raw)
		raw = strings.ReplaceAll(raw, `\n`, "\n")
		var t T
		dec := json.NewDecoder(bytes.NewReader(data))
		err := dec.Decode(&t)
		var tail interface{}
		err2 := dec.Decode(&tail)
		tok, ok := dec.Token()
		fmt.Printf("--- %s ---\n", name)
		fmt.Printf("raw=%q\n", raw)
		fmt.Printf("decode err=%v\n", err)
		if err == nil {
			fmt.Printf("result=%+v\n", t)
			fmt.Printf("second decode err=%v\n", err2)
		}
		if ok {
			fmt.Printf("next token=%T %#v\n", tok, tok)
		} else {
			fmt.Printf("!NextToken ok=false\n")
		}
	}
}
GO
  (cd "$tmp" && go run main.go)
else
  echo 'go not available'
fi

Repository: larksuite/cli

Length of output: 5803


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '## fixed Go decoder behavior probe'
tmp="$(mktemp -d)"
cat > "$tmp/main.go" <<'GO'
package main
import (
	"bytes"
	"encoding/json"
	"fmt"
	"strings"
)
type T struct { Folder string `json:"folder"` }
func main() {
	tests := map[string]string{
		"concatenated":    `{"folder":"INBOX"} {"is_unread":true}`,
		"concatenated_nl": "{\"folder\":\"INBOX\"}\n {\"is_unread\":true}",
		"null":             `null`,
		"array":            `["INBOX"]`,
		"plain_object":     `{"folder":"INBOX"}`,
		"empty_object":     `{}`,
	}
	for name, raw := range tests {
		var t T
		dec := json.NewDecoder(bytes.NewReader([]byte(raw)))
		err := dec.Decode(&t)
		var tail interface{}
		err2 := dec.Decode(&tail)
		fmt.Printf("--- %s --- raw=%q\n", name, raw)
		fmt.Printf("decode err=%v result=%+v\n", err, t)
		fmt.Printf("second decode err=%v tail=%v\n", err2, tail)
		tok, ok := dec.Token()
		if ok {
			fmt.Printf("next token=%T %#v\n", tok, tok)
		} else {
			fmt.Printf("next token ok=false\n")
		}
	}
}
GO
(cd "$tmp" && go run main.go)

Repository: larksuite/cli

Length of output: 312


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
cat > "$tmp/main.go" <<'GO'
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
)

type T struct{ Folder string `json:"folder"` }

func main() {
	tests := map[string]string{
		"concatenated":    `{"folder":"INBOX"} {"is_unread":true}`,
		"concatenated_nl": "{\"folder\":\"INBOX\"}\n {\"is_unread\":true}",
		"null":             `null`,
		"array":            `["INBOX"]`,
		"plain_object":     `{"folder":"INBOX"}`,
		"empty_object":     `{}`,
	}
	for name, raw := range tests {
		var t T
		dec := json.NewDecoder(bytes.NewReader([]byte(raw)))
		err := dec.Decode(&t)
		var tail interface{}
		err2 := dec.Decode(&tail)
		tok, ok := dec.Token()
		next := "<EOF>"
		if ok {
			next = fmt.Sprintf("%T %#v", tok, tok)
		}
		fmt.Printf("--- %s --- raw=%q\n", name, raw)
		fmt.Printf("decode err=%v result=%+v\n", err, t)
		fmt.Printf("second decode err=%v tail=%v\n", err2, tail)
		fmt.Printf("next token=%s\n", next)
	}
}
GO
(cd "$tmp" && go run main.go)

Repository: larksuite/cli

Length of output: 230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
cat > "$tmp/main.go" <<'GO'
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
)

type T struct{ Folder string `json:"folder"` }

func main() {
	tests := map[string]string{
		"concatenated":    `{"folder":"INBOX"} {"is_unread":true}`,
		"concatenated_nl": "{\"folder\":\"INBOX\"}\n {\"is_unread\":true}",
		"null":             `null`,
		"array":            `["INBOX"]`,
		"plain_object":     `{"folder":"INBOX"}`,
		"empty_object":     `{}`,
	}
	for name, raw := range tests {
		var t T
		dec := json.NewDecoder(bytes.NewReader([]byte(raw)))
		err := dec.Decode(&t)
		var tail interface{}
		err2 := dec.Decode(&tail)
		var next string
		tok, ok := dec.Token()
		if ok {
			next = fmt.Sprintf("%T %#v", tok, tok)
		} else {
			next = "<EOF>"
		}
		fmt.Printf("--- %s --- raw=%q\n", name, raw)
		fmt.Printf("decode err=%v result=%+v\n", err, t)
		fmt.Printf("second decode err=%v tail=%v\n", err2, tail)
		fmt.Printf("next token=%s\n", next)
	}
}
GO
(cd "$tmp" && go run main.go)

Repository: larksuite/cli

Length of output: 230


🌐 Web query:

Go encoding/json Decoder Decode reads one JSON value and trailing characters invalid input

💡 Result:

In Go, the encoding/json Decoder.Decode method is designed to read and parse a single JSON value from an input stream [1][2]. By design, it consumes only the bytes necessary to form that valid JSON value [1]. If the input stream contains additional data after the valid JSON value, the Decoder does not treat this as an error; the trailing characters remain in the underlying buffer or the io.Reader, effectively left for subsequent operations [3][4]. This behavior can often lead to confusion when developers assume Decode will validate that a stream contains exactly one JSON value [3]. Because the decoder may buffer more data than is required for the current value, those "leftover" bytes—including trailing whitespace or subsequent JSON objects—are not automatically discarded or validated [4]. If you need to ensure that no trailing data exists or to process multiple JSON values, you can use one of the following patterns: 1. Check for EOF: After calling Decode, verify that no meaningful data remains by calling the Decoder.Token method. If there is no more data, Token will return io.EOF [3][2]. Example: d:= json.NewDecoder(r) if err:= d.Decode(&v); err!= nil { return err } if _, err:= d.Token; err!= io.EOF { return fmt.Errorf("unexpected trailing data") } 2. Inspect the Buffer: You can use the Decoder.Buffered method to inspect any data that has been read from the io.Reader but not yet consumed by a Decode or Token call [2][4]. Note that this only returns the data currently held in the internal buffer, not the entire remainder of the io.Reader [4][5]. This design allows the Decoder to support streaming multiple JSON objects from a single source, which is a primary use case for the package [3]. If the input contains invalid JSON syntax after the first object, the decoder will only report an error if it attempts to parse that specific section as part of a subsequent value [1][6].

Citations:


Reject trailing and non-object JSON filters.

json.Decoder.Decode only reads one JSON value, so --filter '{"folder":"INBOX"} {"is_unread":true}' applies only the first object, and --filter null decodes as an empty triageFilter. Require a single JSON object and EOF after decoding, returning a typed --filter validation error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/mail_triage.go` around lines 442 - 454, The JSON parsing flow
around the decoder in the triage filter parser must require exactly one non-null
JSON object. Validate that decoding produces an object rather than null or
another JSON type, then perform a second decode to confirm EOF and reject
trailing values; return a typed --filter validation error for either condition
while preserving existing unknown-field hints and shorthand fallback for initial
JSON decode failures.

Source: Coding guidelines

| 参数 | 默认 | 说明 |
|------|------|------|
| `--filter <json>` | — | 筛选条件(见下方字段说明) |
| `--filter <json|shorthand>` | — | 筛选条件(见下方字段说明)。支持 JSON,也支持 `folder_id=DRAFT`、`folder=INBOX`、`is_unread`/`unread`、`is_read`/`read` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Escape the pipe in the table cell.

<json|shorthand> creates an extra Markdown column, so this row renders incorrectly. Use <json-or-shorthand> or escape the pipe.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 52-52: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-mail/references/lark-mail-triage.md` at line 52, Update the
option description row near the --filter entry so the angle-bracket placeholder
does not contain an unescaped pipe; replace it with a pipe-free equivalent such
as json-or-shorthand, preserving the documented support for both formats.

Source: Linters/SAST tools

@github-actions

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@315d58c222c7f0b00dc1933dc565b60754bd8e17

🧩 Skill update

npx skills add bubbmon233/cli#feat/dc70da0 -y -g

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/mail PR touches the mail domain size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants