feat(mail): support triage filter shorthand#2025
Conversation
📝 WalkthroughWalkthrough
ChangesMail triage filter handling
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
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
shortcuts/mail/mail_triage.goshortcuts/mail/mail_triage_test.goskills/lark-mail/SKILL.mdskills/lark-mail/references/lark-mail-triage.md
| 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 |
There was a problem hiding this comment.
🎯 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 || trueRepository: 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'
fiRepository: 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'
fiRepository: 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:
- 1: https://go.dev/src/encoding/json/stream.go?m=text
- 2: https://pkg.go.dev/encoding/json
- 3: encoding/json: the Decoder.Decode API lends itself to misuse golang/go#36225
- 4: https://github.com/golang/go/blob/master/src/encoding/json/stream.go
- 5: encoding/json: Decoder.Buffered omits trailing newline after Token under jsonv2 at buffer boundary golang/go#79498
- 6: Reading stream with json.NewDecoder fails with "invalid character '\r' in string literal" if large base64 []byte present golang/go#39874
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` | |
There was a problem hiding this comment.
📐 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
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@315d58c222c7f0b00dc1933dc565b60754bd8e17🧩 Skill updatenpx skills add bubbmon233/cli#feat/dc70da0 -y -g |
Adds shorthand parsing for mail triage filters and a separate folder flag, while keeping the existing JSON filter flow.
Summary by CodeRabbit
New Features
mail +triage, including unread, read, folder, and folder ID filters.--foldersupport that combines with compatible filters and provides clearer conflict validation.Bug Fixes
is_unread=false.Documentation