Skip to content

feat(mail): support triage filter shorthand#2025

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

feat(mail): support triage filter shorthand#2025
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

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.

1 participant