Skip to content

feat(fetch): add multi-entity content reads - #2202

Open
jeremy666-cjl wants to merge 1 commit into
larksuite:mainfrom
jeremy666-cjl:feat/multi-entity-fetch
Open

feat(fetch): add multi-entity content reads#2202
jeremy666-cjl wants to merge 1 commit into
larksuite:mainfrom
jeremy666-cjl:feat/multi-entity-fetch

Conversation

@jeremy666-cjl

@jeremy666-cjl jeremy666-cjl commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Add a unified drive +fetch shortcut for readable Markdown snapshots across Docs, Sheets, Base, Slides, Drive files, Minutes, and Wiki-backed resources. Reuse the shared content-read path for whole-document Markdown in docs +fetch, and update the embedded Skills to route overview reads while preserving entity-native structured and editing workflows.

Changes

  • Add a shared content-read client and Markdown renderer with block anchors, image rendering, GFM table materialization, configurable row limits, and pagination support.
  • Add drive +fetch with URL auto-detection or explicit token/type input, Wiki unwrapping and source metadata, selector preservation, Minutes artifacts, and a consistent resource envelope.
  • Route whole-document Markdown reads in docs +fetch through the paginated anchored-Markdown path, while preserving scoped/XML reads and the document API path for historical revisions or explicit language selection.
  • Preserve typed API errors, recovery metadata, log IDs, and causes across dispatch and fallback paths; reject incompatible flags and report incomplete pagination without discarding readable content.
  • Safely deliver oversized --full responses through private temporary Markdown files after content-safety scanning, with inline fallback when local file delivery is unavailable.
  • Update the Doc, Drive, Base, Sheets, Slides, and Minutes Skills so overview and cross-section reads use the Markdown path, while precise structured reads and editing workflows remain on entity-native commands.
  • Add unit, dry-run E2E, and opt-in live workflow coverage for rendering, pagination, Wiki dispatch, Minutes degradation, typed errors, content safety, and oversized-output delivery.

Test Plan

  • go vet ./...
  • gofmt -l . produces no output
  • go mod tidy produces no module changes
  • go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=upstream/main reports 0 issues
  • node scripts/skill-format-check/index.js skills
  • Docs and Drive fetch dry-run E2E suites
  • Manual live verification with the candidate CLI and embedded Skills for representative Doc/Wiki scoped reads, a Slides full overview, and a Minutes overview
  • make unit-test: every candidate-touched package passes, but the full run currently hits the upstream shortcuts/im failure documented below

Upstream baseline note: upstream/main at b546516b fails TestAllIMShortcutsUseAffordanceExamples/+messages-search because the newly dual-identity shortcut example still pins --as user. The same failure reproduces in a clean worktree at b546516b; this PR does not modify any IM files.

Related Issues

  • None

Summary by CodeRabbit

  • New Features

    • Added drive +fetch for unified Markdown retrieval across documents, sheets, bases, slides, files, Minutes, and Wiki content.
    • Added pagination, full-document retrieval, dry-run validation, resource selectors, and Wiki fallback handling.
    • Added anchored Markdown rendering for headings, lists, code, images, tables, embeds, and whiteboards.
    • Large responses can be saved securely to temporary Markdown files with size, hash, encoding, and preview metadata.
    • Added Minutes summaries with optional transcripts, notes, chapters, todos, and keywords.
  • Bug Fixes

    • Improved safety-scan consistency and inline fallback messaging for file-delivery failures.
  • Documentation

    • Updated command guidance and references for unified fetching, pagination, and large-content delivery.

@CLAassistant

CLAassistant commented Aug 6, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added domain/base PR touches the base domain domain/ccm PR touches the ccm domain size/XL Architecture-level or global-impact change labels Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds shared Markdown content fetching, anchored XML rendering, pagination, Wiki resolution, temporary-file delivery, safety-result reuse, and the drive +fetch shortcut. It also adds Minutes rendering, CLI validation, end-to-end tests, and updated skill documentation.

Unified fetch and delivery

Layer / File(s) Summary
Temporary files and safety-aware delivery
extension/fileio/types.go, internal/contentartifact/*, internal/output/*, internal/vfs/localfileio/*, shortcuts/common/fetch_content_delivery.go, shortcuts/common/runner.go
Adds local temporary-file capability detection, secure Markdown spill files, cleanup on write failures, reusable safety results, and inline fallback metadata.
Content-read API and Markdown rendering
shortcuts/common/contentread/*
Adds typed content-read requests and responses, paginated fetching, anchored XML-to-Markdown rendering, image handling, embedded resources, table formatting, and truncation.
Wiki resolution and document Markdown fetch
shortcuts/common/resource_url.go, shortcuts/common/wiki_node.go, shortcuts/doc/docs_fetch_markdown.go, shortcuts/doc/docs_fetch_v2.go, shortcuts/doc/*_test.go
Adds Wiki node resolution, URL reconstruction, anchored Markdown reads, pagination flags, fallback handling, typed diagnostics, and cached Wiki probing.
Drive fetch routing and output
shortcuts/drive/*
Adds drive +fetch input parsing, resource dispatch, Wiki and Minutes handling, dry-run planning, unified envelopes, warnings, and content delivery.
Minutes rendering and command guidance
shortcuts/minutes/*, skills/*, tests/cli_e2e/*
Adds Minutes Markdown rendering with optional transcript and note content, updates routing guidance, and adds dry-run and temporary-file workflow coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: feature, enhancement

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: unified content reads across multiple entities.
Description check ✅ Passed The description includes all required sections and provides detailed scope, changes, testing, and the documented unrelated test failure.
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.

@jeremy666-cjl
jeremy666-cjl force-pushed the feat/multi-entity-fetch branch from f416990 to addd32f Compare August 6, 2026 03:30

@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: 11

🧹 Nitpick comments (13)
shortcuts/common/fetch_content_delivery.go (1)

56-57: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Avoid the redundant string-to-byte conversions.

len([]byte(content)) copies the whole body only to measure it. len(content) on a string already returns the byte length without allocating. Line 71 then makes a second full copy. For a body above the 24 KiB threshold this allocates the content twice per call before any file is written.

Note that body := []byte(content) at Line 71 is still required, because WriteTempMarkdown and sha256.Sum256 need a byte slice.

♻️ Proposed change to measure the length without copying
 	delivery := FetchContentDelivery{Content: content}
 	autoSpill := runtime.Bool("full") &&
-		runtime.JqExpr == "" && len([]byte(content)) > FetchContentSpillThreshold
+		runtime.JqExpr == "" && len(content) > FetchContentSpillThreshold
 	if !autoSpill {
 		return delivery, scan, nil
 	}
🤖 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/common/fetch_content_delivery.go` around lines 56 - 57, Update the
autoSpill condition in the content-fetch flow to use len(content) instead of
converting content to []byte for measurement, while preserving the existing
runtime.Bool, JqExpr, and threshold checks. Keep the body := []byte(content)
conversion used later by WriteTempMarkdown and sha256.Sum256.
shortcuts/common/contentread/fetch.go (1)

26-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why ctx is discarded.

FetchDocInfo accepts ctx and drops it at Line 27. CallAPITyped uses the context stored in the RuntimeContext (shortcuts/common/runner.go Line 291). A caller that passes a derived context with a deadline or cancellation gets no effect, and the call site gives no warning.

Add a short comment next to _ = ctx, or remove the parameter.

🤖 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/common/contentread/fetch.go` around lines 26 - 28, Add a concise
comment next to the `_ = ctx` statement in `FetchDocInfo` explaining that
`CallAPITyped` uses the context held by `RuntimeContext`, so the method’s
supplied context is intentionally unused.
shortcuts/common/contentread/anchored_markdown.go (1)

316-336: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Align readText EOF handling with the other collectors.

readText returns any decoder error, including io.EOF. renderChildren (Line 90), collectRows (Line 264), and collectCells (Line 293) all treat io.EOF as a normal end and return the text collected so far.

If the content stream ends inside an element, readText propagates io.EOF up to FetchAnchoredMarkdown, which converts it to invalid_response and discards every rendered block. The sibling paths would instead return the partial content. Make the behavior consistent so a truncated tail does not fail the whole read.

♻️ Proposed EOF handling in `readText`
 	for {
 		tok, err := dec.Token()
+		if errors.Is(err, io.EOF) {
+			return b.String(), nil
+		}
 		if err != nil {
 			return b.String(), err
 		}
🤖 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/common/contentread/anchored_markdown.go` around lines 316 - 336,
Update anchoredMarkdownRenderer.readText to treat io.EOF from dec.Token as
normal completion, returning the text collected so far with no error, while
continuing to propagate other decoder errors. Keep the existing depth-based
end-element handling unchanged.
shortcuts/common/contentread/tables.go (1)

26-26: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Require a pipe in the delimiter line to avoid false table detection.

gfmDelimiterRe matches a bare thematic break such as ---. If a prose line contains a pipe and the next line is ---, Line 48 treats the pair as a table header plus delimiter. The following pipe-containing prose lines are then consumed as table rows and can be dropped with a truncation hint.

A GFM delimiter row must contain at least one pipe when the table has more than one column, and a single-column table still uses a leading or trailing pipe in practice. Add that check.

♻️ Proposed narrowing of the delimiter check
-		if !inFence && i+1 < len(lines) && strings.Contains(line, "|") && gfmDelimiterRe.MatchString(lines[i+1]) {
+		if !inFence && i+1 < len(lines) && strings.Contains(line, "|") &&
+			strings.Contains(lines[i+1], "|") && gfmDelimiterRe.MatchString(lines[i+1]) {

Also applies to: 48-48

🤖 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/common/contentread/tables.go` at line 26, Update gfmDelimiterRe and
its use in the table-detection logic around the line-48 check so delimiter rows
must contain at least one pipe character. Preserve matching of valid GFM
delimiter rows, including single-column rows with a leading or trailing pipe,
while rejecting bare thematic breaks such as "---" to prevent prose from being
consumed as table rows.
shortcuts/doc/docs_skill_doc_test.go (1)

24-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fail instead of skipping when the skill doc is missing.

lark-doc-fetch.md is a tracked file in this repository. If it is renamed or deleted, this test skips and the flag-drift guard silently stops running. Use t.Fatalf so the loss of the guard is visible.

♻️ Proposed fix
 	data, err := os.ReadFile(filepath.Join("..", "..", "skills", "lark-doc", "references", "lark-doc-fetch.md"))
 	if err != nil {
-		t.Skipf("skill doc not found: %v", err)
+		t.Fatalf("read skill doc: %v", err)
 	}
🤖 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/doc/docs_skill_doc_test.go` around lines 24 - 27, Update the
os.ReadFile error handling in the test to call t.Fatalf instead of t.Skipf when
lark-doc-fetch.md cannot be read, ensuring the flag-drift guard fails visibly
when the tracked skill document is missing.
shortcuts/doc/docs_fetch_v2_test.go (1)

1022-1024: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Register embed-max-rows in the test runtime.

Production registers embed-max-rows with default 50 in v2FetchFlags. This fixture omits it. runtime.Int("embed-max-rows") swallows the lookup error and returns 0, which means "no limit". Any future test that reaches dryRunAnchoredMarkdownFetch through this runtime will assert the wrong row cap and still pass.

♻️ Proposed fixture fix
 	cmd.Flags().Bool("full", false, "")
 	cmd.Flags().String("page-token", "", "")
 	cmd.Flags().Int("page-size", 0, "")
+	cmd.Flags().Int("embed-max-rows", fetchDefaultInt("embed-max-rows"), "")
🤖 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/doc/docs_fetch_v2_test.go` around lines 1022 - 1024, Update the
test command fixture that registers flags alongside “full”, “page-token”, and
“page-size” to also register “embed-max-rows” with the production default of 50,
so runtime.Int("embed-max-rows") observes the configured row cap.
tests/cli_e2e/docs/docs_fetch_dryrun_test.go (1)

56-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the typed error envelope fields, not a stderr substring.

require.Contains(t, result.Stderr, "--page-size") passes even when error.param names a different flag, because the flag name also appears in the message text. Parse the JSON envelope from stderr and assert error.type, error.subtype, and error.param.

♻️ Proposed assertions
 			require.NoError(t, err)
 			result.AssertExitCode(t, 2)
-			require.Contains(t, result.Stderr, "--page-size")
+			require.Equal(t, "validation", gjson.Get(result.Stderr, "error.type").String(), "stderr:\n%s", result.Stderr)
+			require.NotEmpty(t, gjson.Get(result.Stderr, "error.subtype").String(), "stderr:\n%s", result.Stderr)
+			require.Equal(t, "--page-size", gjson.Get(result.Stderr, "error.param").String(), "stderr:\n%s", result.Stderr)

Based on learnings: "Validate-stage failures must exit with code 2, write the typed JSON validation envelope to result.Stderr... Parse and assert error.type, error.subtype, error.param, and error.message from stderr."

🤖 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 `@tests/cli_e2e/docs/docs_fetch_dryrun_test.go` around lines 56 - 58, Replace
the stderr substring assertion in the dry-run fetch test with JSON envelope
parsing. Assert the typed validation fields error.type, error.subtype,
error.param, and error.message, while preserving the expected exit code 2 and
stderr source.

Source: Learnings

shortcuts/drive/drive_fetch_envelope.go (1)

83-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct the withPagination doc comment.

The comment states "No-op when hasMore is false". The body assigns HasMore and NextPageToken unconditionally. Align the comment with the behavior.

📝 Proposed fix
 // withPagination records a pagination cursor so a --format json
-// consumer sees has_more / next_page_token alongside the content. No-op when
-// hasMore is false.
+// consumer sees has_more / next_page_token alongside the content. Both fields
+// use omitempty, so a false hasMore and an empty cursor are omitted from JSON.
 func (e *fetchEnvelope) withPagination(hasMore bool, nextToken string) *fetchEnvelope {
🤖 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/drive/drive_fetch_envelope.go` around lines 83 - 90, Update the doc
comment for fetchEnvelope.withPagination to remove the inaccurate “No-op when
hasMore is false” statement and describe that it always assigns HasMore and the
trimmed NextPageToken.
shortcuts/minutes/minutes_fetch.go (2)

106-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the local context variable; it shadows the imported context package.

The context package is imported at Line 7. The local slice at Line 106 hides it inside this function. Rename it to keep the package name available.

♻️ Proposed fix
-	var context []string
+	var details []string
 	if hint := strings.TrimSpace(problem.Hint); hint != "" && !strings.Contains(warning, hint) {
-		context = append(context, "hint: "+hint)
+		details = append(details, "hint: "+hint)
 	}
 	if logID := strings.TrimSpace(problem.LogID); logID != "" {
-		context = append(context, "log_id: "+logID)
+		details = append(details, "log_id: "+logID)
 	}
-	if len(context) > 0 {
-		warning += " (" + strings.Join(context, "; ") + ")"
+	if len(details) > 0 {
+		warning += " (" + strings.Join(details, "; ") + ")"
 	}
🤖 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/minutes/minutes_fetch.go` around lines 106 - 115, Rename the local
slice variable context in the warning-enrichment block to a non-conflicting
name, and update its append, length, and strings.Join references while
preserving the existing hint and logID behavior. Keep the imported context
package accessible within the function.

120-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Attach --include to the validation error.

ParseIncludes returns a typed validation error without a param. Both callers surface it for the --include flag. Setting the param lets the CLI name the flag and lets error-path tests assert the metadata.

♻️ Proposed fix
-			return nil, common.ValidationErrorf("invalid --include value %q (allowed: transcript, note-doc)", v)
+			return nil, common.ValidationErrorf("invalid --include value %q (allowed: transcript, note-doc)", v).WithParam("--include")

As per coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param)".

🤖 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/minutes/minutes_fetch.go` around lines 120 - 129, Update
ParseIncludes to attach the --include parameter when constructing the validation
error for an invalid value, using the existing typed validation-error API so
errs.ProblemOf reports the expected category, subtype, and param metadata. Keep
the valid-value parsing and returned set unchanged.

Source: Coding guidelines

shortcuts/drive/drive_fetch_dispatch.go (1)

219-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated contentread.FetchOptions construction.

The same four-field FetchOptions literal appears at Lines 29-34, Lines 67-72, and Lines 230-235. A single helper keeps the flag names in one place and prevents drift when a pagination flag is renamed.

♻️ Proposed helper
+// fetchOptionsFor builds the shared content-read options from the fetch flags.
+func fetchOptionsFor(runtime *common.RuntimeContext) contentread.FetchOptions {
+	return contentread.FetchOptions{
+		MaxRows:   runtime.Int("embed-max-rows"),
+		Full:      runtime.Bool("full"),
+		PageToken: strings.TrimSpace(runtime.Str("page-token")),
+		PageSize:  runtime.Int("page-size"),
+	}
+}

Then each call site becomes:

-	maxRows := runtime.Int("embed-max-rows")
-	opts := contentread.FetchOptions{
-		MaxRows:   maxRows,
-		Full:      runtime.Bool("full"),
-		PageToken: strings.TrimSpace(runtime.Str("page-token")),
-		PageSize:  runtime.Int("page-size"),
-	}
+	opts := fetchOptionsFor(runtime)
🤖 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/drive/drive_fetch_dispatch.go` around lines 219 - 250, Extract the
repeated contentread.FetchOptions construction into a shared helper near the
fetch functions, using the existing runtime values for embed-max-rows, full,
page-token, and page-size. Update fetchWikiDirect and the other FetchMarkdown
call sites that currently build the same literal to use this helper, keeping all
existing pagination behavior unchanged.
shortcuts/minutes/minutes_fetch_test.go (2)

93-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the metadata optionalMinutesWarning embeds.

This test asserts only the "note documents omitted" prefix. The hint and log_id embedding in optionalMinutesWarning (shortcuts/minutes/minutes_fetch.go Lines 107-115) is not asserted by any test in this file, so deleting that block would not fail the suite.

Extend this test, or add one, so the warning string is asserted to carry the typed hint and the log ID from the upstream failure. The degraded value is a string by design, so assert the embedded metadata rather than calling errs.ProblemOf on the warning.

As per coding guidelines: "Every behavior change must have an accompanying test, and contract tests must assert the changed field or behavior directly so reverting the implementation causes failure."

🤖 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/minutes/minutes_fetch_test.go` around lines 93 - 108, Extend
TestFetchMinutesMarkdownNoteFailureDegradesToWarning to assert that the warning
includes the typed hint and upstream log ID produced by optionalMinutesWarning,
in addition to the existing “note documents omitted” text. Read the metadata
from the warning string directly rather than using errs.ProblemOf, and configure
the mocked failure with stable hint and log ID values so reverting
optionalMinutesWarning would fail the test.

Source: Coding guidelines


141-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard against a missing title before comparing positions.

strings.Index returns -1 for an absent title. If renderChapters dropped a chapter, -1 < pos("Untimed") still holds and the ordering assertion passes. Assert that every position is non-negative first, so the regression guard cannot pass on missing output. The same applies to Lines 161-165.

🤖 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/minutes/minutes_fetch_test.go` around lines 141 - 147, Update the
ordering assertions around renderChapters and the corresponding check at lines
161-165 to first verify every strings.Index result is non-negative, then compare
the positions. Preserve the existing failure reporting while ensuring missing
chapter titles cannot satisfy the ordering condition.
🤖 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/common/contentread/anchored_markdown_test.go`:
- Around line 263-265: Update both assertions in the anchored Markdown tests
near the expanded synced-content checks to verify that the rendered output does
not contain the actual placeholder marker “内容可能未展开” emitted by renderEmbedTable.
Remove the ineffective “base 技能” checks so reverting the expanded-text behavior
causes these contract tests to fail.

In `@shortcuts/common/contentread/fetch_test.go`:
- Around line 187-189: Update the HTTP 500 assertion in the FetchDocInfo test to
extract the typed problem with errs.ProblemOf, then assert its Category,
populated Subtype, and param metadata while preserving the existing non-nil
error check. Also verify the underlying cause is preserved, without requiring a
specific Subtype constant.

In `@shortcuts/common/contentread/pagination.go`:
- Around line 15-17: Update ApplyPagination to validate pageSize before
converting it to int32, rejecting values above math.MaxInt32 rather than
allowing overflow or truncation; preserve the existing handling for positive
in-range values and non-positive values. Prefer propagating a typed validation
error at the CLI flag boundary if the surrounding callers support validation
errors.

In `@shortcuts/common/fetch_content_delivery_test.go`:
- Around line 279-282: Add an errors.Is(err, scan.BlockErr) assertion to the
PrepareFetchContentDelivery test alongside the existing blocked-result checks,
verifying that the returned error preserves scan.BlockErr as its cause while
retaining the existing ContentSafetyError classification coverage.

In `@shortcuts/doc/docs_fetch_v2_test.go`:
- Around line 1086-1100: Update TestValidatePaginatedReadFlagsRevisionConflict
to assert typed validation metadata instead of only matching err.Error(). Use
errs.ProblemOf to verify category and subtype, then use errors.As to extract
*errs.ValidationError and assert its Param value; preserve the existing conflict
scenario and failure expectations.

In `@shortcuts/doc/docs_fetch_v2_wiki_test.go`:
- Around line 410-426: Add a test covering ResolveFetchURLDetailed and the
wikiNodeURL shortcut branch for a node_type=shortcut response containing
origin_node_token. Assert that the resolved contracted URL targets the origin
node token, using wikiNodeStub or an equivalent stub setup and preserving
existing ParseResourceURL/BuildResourceURL tests.

In `@shortcuts/drive/drive_fetch_test.go`:
- Around line 69-76: Update the validation-error assertions in
shortcuts/drive/drive_fetch_test.go at lines 69-76 to use errs.ProblemOf for the
expected category and subtype, while retaining errors.As to verify
ValidationError.Param and the existing hint check. In
tests/cli_e2e/drive/drive_fetch_dryrun_test.go at lines 142-207, parse the
structured validation envelope from stderr for every rejected flag combination
and assert its error type, subtype, and parameter.

In `@shortcuts/drive/drive_fetch_validate.go`:
- Around line 31-33: Update the minutes validation branch in drive fetch
validation to remove the redundant fetchType interpolation and report the
pagination flag actually supplied. Add a firstPaginationFlag helper near
hasPaginationFlags, checking --full, --page-token, then --page-size, and use its
result for both the error message context and WithParam instead of the fixed
--full.

In `@shortcuts/drive/drive_skill_doc_test.go`:
- Around line 24-27: Replace the os.ReadFile call in the skill document test
with the repository filesystem abstraction from internal/vfs, using it to read
the committed lark-drive-fetch.md document while preserving the existing error
handling and assertion flow.

In `@skills/lark-drive/references/lark-drive-fetch.md`:
- Line 53: Update the `--token` + `--type` documentation row to remove `doc`
from the listed aliases, preserving only `sheets` → `sheet` and `base` →
`bitable`; keep `docx` as the distinct supported type.

In `@skills/lark-slides/SKILL.md`:
- Around line 88-89: Update the “读取指定页、检查版式 / 元素结构,或编辑前回读” guidance to
distinguish `slides +xml-get` as the shortcut from `xml_presentation.slide.get`
as the native API, and specify the correct command form for single-slide reads
while preserving the existing token and ID requirements.

---

Nitpick comments:
In `@shortcuts/common/contentread/anchored_markdown.go`:
- Around line 316-336: Update anchoredMarkdownRenderer.readText to treat io.EOF
from dec.Token as normal completion, returning the text collected so far with no
error, while continuing to propagate other decoder errors. Keep the existing
depth-based end-element handling unchanged.

In `@shortcuts/common/contentread/fetch.go`:
- Around line 26-28: Add a concise comment next to the `_ = ctx` statement in
`FetchDocInfo` explaining that `CallAPITyped` uses the context held by
`RuntimeContext`, so the method’s supplied context is intentionally unused.

In `@shortcuts/common/contentread/tables.go`:
- Line 26: Update gfmDelimiterRe and its use in the table-detection logic around
the line-48 check so delimiter rows must contain at least one pipe character.
Preserve matching of valid GFM delimiter rows, including single-column rows with
a leading or trailing pipe, while rejecting bare thematic breaks such as "---"
to prevent prose from being consumed as table rows.

In `@shortcuts/common/fetch_content_delivery.go`:
- Around line 56-57: Update the autoSpill condition in the content-fetch flow to
use len(content) instead of converting content to []byte for measurement, while
preserving the existing runtime.Bool, JqExpr, and threshold checks. Keep the
body := []byte(content) conversion used later by WriteTempMarkdown and
sha256.Sum256.

In `@shortcuts/doc/docs_fetch_v2_test.go`:
- Around line 1022-1024: Update the test command fixture that registers flags
alongside “full”, “page-token”, and “page-size” to also register
“embed-max-rows” with the production default of 50, so
runtime.Int("embed-max-rows") observes the configured row cap.

In `@shortcuts/doc/docs_skill_doc_test.go`:
- Around line 24-27: Update the os.ReadFile error handling in the test to call
t.Fatalf instead of t.Skipf when lark-doc-fetch.md cannot be read, ensuring the
flag-drift guard fails visibly when the tracked skill document is missing.

In `@shortcuts/drive/drive_fetch_dispatch.go`:
- Around line 219-250: Extract the repeated contentread.FetchOptions
construction into a shared helper near the fetch functions, using the existing
runtime values for embed-max-rows, full, page-token, and page-size. Update
fetchWikiDirect and the other FetchMarkdown call sites that currently build the
same literal to use this helper, keeping all existing pagination behavior
unchanged.

In `@shortcuts/drive/drive_fetch_envelope.go`:
- Around line 83-90: Update the doc comment for fetchEnvelope.withPagination to
remove the inaccurate “No-op when hasMore is false” statement and describe that
it always assigns HasMore and the trimmed NextPageToken.

In `@shortcuts/minutes/minutes_fetch_test.go`:
- Around line 93-108: Extend
TestFetchMinutesMarkdownNoteFailureDegradesToWarning to assert that the warning
includes the typed hint and upstream log ID produced by optionalMinutesWarning,
in addition to the existing “note documents omitted” text. Read the metadata
from the warning string directly rather than using errs.ProblemOf, and configure
the mocked failure with stable hint and log ID values so reverting
optionalMinutesWarning would fail the test.
- Around line 141-147: Update the ordering assertions around renderChapters and
the corresponding check at lines 161-165 to first verify every strings.Index
result is non-negative, then compare the positions. Preserve the existing
failure reporting while ensuring missing chapter titles cannot satisfy the
ordering condition.

In `@shortcuts/minutes/minutes_fetch.go`:
- Around line 106-115: Rename the local slice variable context in the
warning-enrichment block to a non-conflicting name, and update its append,
length, and strings.Join references while preserving the existing hint and logID
behavior. Keep the imported context package accessible within the function.
- Around line 120-129: Update ParseIncludes to attach the --include parameter
when constructing the validation error for an invalid value, using the existing
typed validation-error API so errs.ProblemOf reports the expected category,
subtype, and param metadata. Keep the valid-value parsing and returned set
unchanged.

In `@tests/cli_e2e/docs/docs_fetch_dryrun_test.go`:
- Around line 56-58: Replace the stderr substring assertion in the dry-run fetch
test with JSON envelope parsing. Assert the typed validation fields error.type,
error.subtype, error.param, and error.message, while preserving the expected
exit code 2 and stderr source.
🪄 Autofix

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: b841c8ce-d93a-4c95-9ced-26d8bd4f73f0

📥 Commits

Reviewing files that changed from the base of the PR and between b546516 and addd32f.

📒 Files selected for processing (55)
  • extension/fileio/types.go
  • internal/contentartifact/temp.go
  • internal/contentartifact/temp_test.go
  • internal/output/emitter.go
  • internal/output/emitter_contract_test.go
  • internal/vfs/localfileio/localfileio.go
  • internal/vfs/localfileio/localfileio_test.go
  • shortcuts/common/contentread/anchored_markdown.go
  • shortcuts/common/contentread/anchored_markdown_table.go
  • shortcuts/common/contentread/anchored_markdown_test.go
  • shortcuts/common/contentread/fetch.go
  • shortcuts/common/contentread/fetch_test.go
  • shortcuts/common/contentread/images.go
  • shortcuts/common/contentread/markdown.go
  • shortcuts/common/contentread/pagination.go
  • shortcuts/common/contentread/postprocess_test.go
  • shortcuts/common/contentread/tables.go
  • shortcuts/common/contentread/types.go
  • shortcuts/common/fetch_content_delivery.go
  • shortcuts/common/fetch_content_delivery_test.go
  • shortcuts/common/resource_url.go
  • shortcuts/common/runner.go
  • shortcuts/common/wiki_node.go
  • shortcuts/doc/docs_fetch_markdown.go
  • shortcuts/doc/docs_fetch_spill_test.go
  • shortcuts/doc/docs_fetch_v2.go
  • shortcuts/doc/docs_fetch_v2_test.go
  • shortcuts/doc/docs_fetch_v2_wiki_test.go
  • shortcuts/doc/docs_skill_doc_test.go
  • shortcuts/drive/drive_fetch.go
  • shortcuts/drive/drive_fetch_dispatch.go
  • shortcuts/drive/drive_fetch_envelope.go
  • shortcuts/drive/drive_fetch_input.go
  • shortcuts/drive/drive_fetch_spill_test.go
  • shortcuts/drive/drive_fetch_test.go
  • shortcuts/drive/drive_fetch_validate.go
  • shortcuts/drive/drive_skill_doc_test.go
  • shortcuts/drive/shortcuts.go
  • shortcuts/drive/shortcuts_test.go
  • shortcuts/minutes/minutes_fetch.go
  • shortcuts/minutes/minutes_fetch_test.go
  • skills/lark-base/SKILL.md
  • skills/lark-doc/SKILL.md
  • skills/lark-doc/references/lark-doc-fetch.md
  • skills/lark-drive/SKILL.md
  • skills/lark-drive/references/lark-drive-fetch.md
  • skills/lark-minutes/SKILL.md
  • skills/lark-sheets/SKILL.md
  • skills/lark-slides/SKILL.md
  • tests/cli_e2e/docs/coverage.md
  • tests/cli_e2e/docs/docs_create_fetch_test.go
  • tests/cli_e2e/docs/docs_fetch_dryrun_test.go
  • tests/cli_e2e/drive/coverage.md
  • tests/cli_e2e/drive/drive_fetch_dryrun_test.go
  • tests/cli_e2e/drive/drive_fetch_workflow_test.go

Comment on lines +263 to +265
if strings.Contains(got, "base 技能") {
t.Errorf("expanded synced must not be a placeholder, got:\n%s", got)
}

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

Assert the real placeholder marker, not base 技能.

renderEmbedTable emits 内容可能未展开 for the placeholder branch (shortcuts/common/contentread/anchored_markdown.go Line 229). The string base 技能 is not produced anywhere in the package. Both assertions therefore always pass, and a revert of the expanded-text branch would not fail these tests.

Assert the absence of 内容可能未展开 instead.

As per coding guidelines: "contract tests must assert the changed field or behavior directly so reverting the implementation causes failure."

💚 Proposed assertion fix for both sites
-	if strings.Contains(got, "base 技能") {
+	if strings.Contains(got, "内容可能未展开") {
 		t.Errorf("expanded synced must not be a placeholder, got:\n%s", got)
 	}
-	if strings.Contains(got, "base 技能") {
+	if strings.Contains(got, "内容可能未展开") {
 		t.Errorf("expanded component must not be a placeholder, got:\n%s", got)
 	}

Also applies to: 282-284

🤖 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/common/contentread/anchored_markdown_test.go` around lines 263 -
265, Update both assertions in the anchored Markdown tests near the expanded
synced-content checks to verify that the rendered output does not contain the
actual placeholder marker “内容可能未展开” emitted by renderEmbedTable. Remove the
ineffective “base 技能” checks so reverting the expanded-text behavior causes
these contract tests to fail.

Source: Coding guidelines

Comment on lines +187 to +189
if _, err := FetchDocInfo(context.Background(), rt, Request{URL: "x"}); err == nil {
t.Fatal("expected error on HTTP 500")
}

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

Assert typed error metadata on the HTTP 500 path.

This test only checks that the error is non-nil. A regression that returns an untyped error still passes. Assert the typed metadata through errs.ProblemOf.

Assert Category and a populated Subtype rather than a specific subtype constant, because the API layer may fall back to SubtypeUnknown.

As per coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param) and verify cause preservation rather than relying only on message substrings."

💚 Proposed typed assertions
-	if _, err := FetchDocInfo(context.Background(), rt, Request{URL: "x"}); err == nil {
-		t.Fatal("expected error on HTTP 500")
-	}
+	_, err := FetchDocInfo(context.Background(), rt, Request{URL: "x"})
+	if err == nil {
+		t.Fatal("expected error on HTTP 500")
+	}
+	p, ok := errs.ProblemOf(err)
+	if !ok {
+		t.Fatalf("expected a typed errs.* error, got %T: %v", err, err)
+	}
+	if p.Category == "" || p.Subtype == "" {
+		t.Errorf("problem metadata incomplete: category=%q subtype=%q", p.Category, p.Subtype)
+	}

Based on learnings: in larksuite/cli Go test suites for shortcuts domain packages, when exercising error paths, avoid asserting a specific Subtype constant that may not exist for that layer; assert Category and verify Subtype is populated.

🤖 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/common/contentread/fetch_test.go` around lines 187 - 189, Update
the HTTP 500 assertion in the FetchDocInfo test to extract the typed problem
with errs.ProblemOf, then assert its Category, populated Subtype, and param
metadata while preserving the existing non-nil error check. Also verify the
underlying cause is preserved, without requiring a specific Subtype constant.

Sources: Coding guidelines, Learnings

Comment on lines +15 to +17
if pageSize > 0 {
req.PageSize = int32(pageSize)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Bound pageSize before narrowing to int32.

pageSize comes from the user-supplied --page-size flag (see shortcuts/drive/drive_fetch_dispatch.go Line 30 and shortcuts/doc/docs_fetch_markdown.go Line 149). On 64-bit platforms a value above math.MaxInt32 wraps and produces a negative or truncated page_size in the request body. The CLI then sends a value the caller did not request.

Reject or clamp out-of-range values instead of coercing them silently.

As per coding guidelines: "never silently coerce unsupported inputs, ignore unhonored options, default missing identities, or discard writes."

🛡️ Proposed clamp inside `ApplyPagination`
 	req.EnablePagination = true
 	req.PageToken = strings.TrimSpace(pageToken)
-	if pageSize > 0 {
-		req.PageSize = int32(pageSize)
-	}
+	if pageSize > 0 {
+		if pageSize > math.MaxInt32 {
+			pageSize = math.MaxInt32
+		}
+		req.PageSize = int32(pageSize)
+	}

Add "math" to the import block. Prefer a typed validation error at the flag boundary if the callers should reject the value instead.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 15-15: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: int32(pageSize)
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)

🤖 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/common/contentread/pagination.go` around lines 15 - 17, Update
ApplyPagination to validate pageSize before converting it to int32, rejecting
values above math.MaxInt32 rather than allowing overflow or truncation; preserve
the existing handling for positive in-range values and non-positive values.
Prefer propagating a typed validation error at the CLI flag boundary if the
surrounding callers support validation errors.

Sources: Coding guidelines, Linters/SAST tools

Comment on lines +279 to +282
delivery, scan, err := PrepareFetchContentDelivery(rctx, map[string]any{"content": content}, content, testFetchContentJQPath)
if err == nil || !scan.Blocked {
t.Fatalf("PrepareFetchContentDelivery() = (%#v, blocked=%t, %v), want content-safety block", delivery, scan.Blocked, err)
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Determine the typed category and subtype produced for a content-safety block.
set -euo pipefail

echo "--- ScanResult and BlockErr construction ---"
rg -nP --type=go -C6 '\bBlockErr\b'

echo "--- ScanForSafety implementation ---"
ast-grep run --pattern 'func ScanForSafety($$$) $_ { $$$ }' --lang go internal/output

Repository: larksuite/cli

Length of output: 196


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- repo files matching test ---"
git ls-files | rg 'shortcuts/common/fetch_content_delivery_test\.go|content.*delivery|fetch.*content|errors|errs|problem' || true

echo "--- target file outline ---"
ast-grep outline shortcuts/common/fetch_content_delivery_test.go --view compact || true

echo "--- target test relevant lines ---"
sed -n '240,310p' shortcuts/common/fetch_content_delivery_test.go || true

echo "--- Search for PrepareFetchContentDelivery definitions/usages ---"
rg -n --type=go -C8 'PrepareFetchContentDelivery|ScanResult|ScanForSafety|BlockErr|content-safety|content safe|unsafe' . || true

Repository: larksuite/cli

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- target test relevant lines ---"
sed -n '260,295p' shortcuts/common/fetch_content_delivery_test.go

echo "--- imports in target test ---"
sed -n '1,40p' shortcuts/common/fetch_content_delivery_test.go

echo "--- implementation around safety scan ---"
sed -n '47,55p' shortcuts/common/fetch_content_delivery.go

echo "--- scan structs and test mocks ---"
rg -n --type=go -C12 'ScanForSafety|ScanResult|runtime\.(\*|Mock).*Scan|func \(.*ScanOutputForSafety|ScanOutputForSafety' shortcuts/common/fetch_content_delivery_test.go shortcuts/common/fetch_content_delivery.go internal/output shortcuts common 2>/dev/null || true

echo "--- focused scan result definitions ---"
rg -n --type=go -C8 'type ScanResult|BlockErr|ScanOutputForSafety' internal shortcuts cmd 2>/dev/null | head -n 240

Repository: larksuite/cli

Length of output: 50369


Assert cause preservation for the block error.

PrepareFetchContentDelivery returns scan.BlockErr directly, so add an errors.Is(err, scan.BlockErr) check. The existing scan test covers the typed ContentSafetyError classification.

🤖 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/common/fetch_content_delivery_test.go` around lines 279 - 282, Add
an errors.Is(err, scan.BlockErr) assertion to the PrepareFetchContentDelivery
test alongside the existing blocked-result checks, verifying that the returned
error preserves scan.BlockErr as its cause while retaining the existing
ContentSafetyError classification coverage.

Source: Coding guidelines

Comment on lines +1086 to +1100
func TestValidatePaginatedReadFlagsRevisionConflict(t *testing.T) {
t.Parallel()
rt := newFetchShortcutTestRuntime(t, "", map[string]string{
"doc-format": "markdown",
"revision-id": "42",
"full": "true",
})
err := validatePaginatedReadFlags(rt)
if err == nil {
t.Fatal("expected conflict error for --full + historical --revision-id")
}
if !strings.Contains(err.Error(), "revision-id") {
t.Fatalf("error should blame --revision-id, got: %v", err)
}
}

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 | 🟠 Major | ⚡ Quick win

Assert typed error metadata instead of a message substring.

This error-path test checks only err.Error(). A message reword breaks the test without a behavior change, and a wrong param or subtype passes it. Assert category, subtype, and param. Read Param through errors.As into *errs.ValidationError, because errs.ProblemOf does not expose it.

♻️ Proposed typed assertions
 	err := validatePaginatedReadFlags(rt)
 	if err == nil {
 		t.Fatal("expected conflict error for --full + historical --revision-id")
 	}
-	if !strings.Contains(err.Error(), "revision-id") {
-		t.Fatalf("error should blame --revision-id, got: %v", err)
+	problem, ok := errs.ProblemOf(err)
+	if !ok || problem.Category != errs.CategoryValidation || problem.Subtype != errs.SubtypeInvalidArgument {
+		t.Fatalf("problem = %#v, want a validation/invalid-argument problem", problem)
+	}
+	var validationErr *errs.ValidationError
+	if !errors.As(err, &validationErr) || validationErr.Param != "--full" {
+		t.Fatalf("param = %#v, want --full", err)
+	}
+	if !strings.Contains(err.Error(), "revision-id") {
+		t.Fatalf("message should name --revision-id, got: %v", err)
 	}

Based on learnings and coding guidelines: "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param)", and errs.ProblemOf returns a Problem without a Param field, so Param must be read via errors.As on *errs.ValidationError.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestValidatePaginatedReadFlagsRevisionConflict(t *testing.T) {
t.Parallel()
rt := newFetchShortcutTestRuntime(t, "", map[string]string{
"doc-format": "markdown",
"revision-id": "42",
"full": "true",
})
err := validatePaginatedReadFlags(rt)
if err == nil {
t.Fatal("expected conflict error for --full + historical --revision-id")
}
if !strings.Contains(err.Error(), "revision-id") {
t.Fatalf("error should blame --revision-id, got: %v", err)
}
}
func TestValidatePaginatedReadFlagsRevisionConflict(t *testing.T) {
t.Parallel()
rt := newFetchShortcutTestRuntime(t, "", map[string]string{
"doc-format": "markdown",
"revision-id": "42",
"full": "true",
})
err := validatePaginatedReadFlags(rt)
if err == nil {
t.Fatal("expected conflict error for --full + historical --revision-id")
}
problem, ok := errs.ProblemOf(err)
if !ok || problem.Category != errs.CategoryValidation || problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("problem = %#v, want a validation/invalid-argument problem", problem)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) || validationErr.Param != "--full" {
t.Fatalf("param = %#v, want --full", err)
}
if !strings.Contains(err.Error(), "revision-id") {
t.Fatalf("message should name --revision-id, got: %v", err)
}
}
🤖 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/doc/docs_fetch_v2_test.go` around lines 1086 - 1100, Update
TestValidatePaginatedReadFlagsRevisionConflict to assert typed validation
metadata instead of only matching err.Error(). Use errs.ProblemOf to verify
category and subtype, then use errors.As to extract *errs.ValidationError and
assert its Param value; preserve the existing conflict scenario and failure
expectations.

Sources: Coding guidelines, Learnings

Comment on lines +69 to +76
err := validateFetchTypeFlags(runtime, "minutes")
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("validateFetchTypeFlags() error = %T %v, want validation error", err, err)
}
if validationErr.Param != "--as" || !strings.Contains(validationErr.Hint, "--as user") {
t.Fatalf("validation error = %#v", validationErr)
}

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 | 🟡 Minor | ⚡ Quick win

Assert the typed validation-error contract in both test layers.

The tests can pass after a category or subtype regression because they primarily assert text.

  • shortcuts/drive/drive_fetch_test.go#L69-L76: Use errs.ProblemOf to assert the validation category and subtype. Keep errors.As to assert ValidationError.Param.
  • tests/cli_e2e/drive/drive_fetch_dryrun_test.go#L142-L207: Parse the structured validation envelope from stderr. Assert error type, subtype, and parameter for each rejected flag combination.

As per coding guidelines, “Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param)”. Based on learnings, errs.ProblemOf does not expose Param; use errors.As for that assertion.

📍 Affects 2 files
  • shortcuts/drive/drive_fetch_test.go#L69-L76 (this comment)
  • tests/cli_e2e/drive/drive_fetch_dryrun_test.go#L142-L207
🤖 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/drive/drive_fetch_test.go` around lines 69 - 76, Update the
validation-error assertions in shortcuts/drive/drive_fetch_test.go at lines
69-76 to use errs.ProblemOf for the expected category and subtype, while
retaining errors.As to verify ValidationError.Param and the existing hint check.
In tests/cli_e2e/drive/drive_fetch_dryrun_test.go at lines 142-207, parse the
structured validation envelope from stderr for every rejected flag combination
and assert its error type, subtype, and parameter.

Sources: Coding guidelines, Learnings

Comment on lines +31 to +33
if hasPaginationFlags(runtime) && fetchType == "minutes" {
return common.ValidationErrorf("--full/--page-token/--page-size do not apply to minutes (got %s)", fetchType).WithParam("--full")
}

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 | 🟡 Minor | ⚡ Quick win

Report the offending pagination flag, and drop the redundant (got %s).

Inside this branch fetchType is always "minutes", so the message renders as "do not apply to minutes (got minutes)". WithParam("--full") is also fixed, so a user who passed only --page-size is pointed at --full.

🐛 Proposed fix
-	if hasPaginationFlags(runtime) && fetchType == "minutes" {
-		return common.ValidationErrorf("--full/--page-token/--page-size do not apply to minutes (got %s)", fetchType).WithParam("--full")
-	}
+	if fetchType == "minutes" {
+		if param := firstPaginationFlag(runtime); param != "" {
+			return common.ValidationErrorf("--full/--page-token/--page-size do not apply to minutes").WithParam(param)
+		}
+	}

Add the helper next to hasPaginationFlags:

// firstPaginationFlag names the pagination flag the caller set, or "" when none is set.
func firstPaginationFlag(runtime *common.RuntimeContext) string {
	switch {
	case runtime.Bool("full"):
		return "--full"
	case strings.TrimSpace(runtime.Str("page-token")) != "":
		return "--page-token"
	case runtime.Int("page-size") > 0:
		return "--page-size"
	}
	return ""
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if hasPaginationFlags(runtime) && fetchType == "minutes" {
return common.ValidationErrorf("--full/--page-token/--page-size do not apply to minutes (got %s)", fetchType).WithParam("--full")
}
if fetchType == "minutes" {
if param := firstPaginationFlag(runtime); param != "" {
return common.ValidationErrorf("--full/--page-token/--page-size do not apply to minutes").WithParam(param)
}
}
🤖 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/drive/drive_fetch_validate.go` around lines 31 - 33, Update the
minutes validation branch in drive fetch validation to remove the redundant
fetchType interpolation and report the pagination flag actually supplied. Add a
firstPaginationFlag helper near hasPaginationFlags, checking --full,
--page-token, then --page-size, and use its result for both the error message
context and WithParam instead of the fixed --full.

Comment on lines +24 to +27
data, err := os.ReadFile(filepath.Join("..", "..", "skills", "lark-drive", "references", "lark-drive-fetch.md"))
if err != nil {
t.Fatalf("skill doc not found: %v", err)
}

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

Use the repository filesystem abstraction.

os.ReadFile bypasses the required internal/vfs filesystem APIs. Read the committed skill document through the repository filesystem abstraction instead. The temporary-fixture exception does not apply to this repository document.

🤖 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/drive/drive_skill_doc_test.go` around lines 24 - 27, Replace the
os.ReadFile call in the skill document test with the repository filesystem
abstraction from internal/vfs, using it to read the committed
lark-drive-fetch.md document while preserving the existing error handling and
assertion flow.

Source: Coding guidelines

| 参数 | 必填 | 说明 |
|---|---|---|
| `--url` | 二选一 | 文档 URL(推荐) |
| `--token` + `--type` | 二选一 | 裸 token 需 `--type`(docx / sheet / bitable / slides / file / minutes / wiki;也接受别名 doc / sheets / base) |

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 | 🟡 Minor | ⚡ Quick win

doc is not an alias of docx.

This row lists doc together with sheets and base as aliases. In normalizeFetchType (shortcuts/drive/drive_fetch_input.go Lines 104-110) only sheetssheet and basebitable are aliases. doc and docx stay distinct, as the comment at Line 27 of that file states. A reader who treats doc as an alias will pass --type doc for a docx token and get a wrong-type request.

📝 Proposed fix
-| `--token` + `--type` | 二选一 | 裸 token 需 `--type`(docx / sheet / bitable / slides / file / minutes / wiki;也接受别名 doc / sheets / base) |
+| `--token` + `--type` | 二选一 | 裸 token 需 `--type`(doc / docx / sheet / bitable / slides / file / minutes / wiki;`doc` 与 `docx` 是不同类型,不可互换;别名:`sheets` = `sheet`,`base` = `bitable`) |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `--token` + `--type` | 二选一 | 裸 token 需 `--type`(docx / sheet / bitable / slides / file / minutes / wiki;也接受别名 doc / sheets / base) |
| `--token` + `--type` | 二选一 | 裸 token 需 `--type`doc / docx / sheet / bitable / slides / file / minutes / wiki;`doc``docx` 是不同类型,不可互换;别名:`sheets` = `sheet``base` = `bitable`|
🤖 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-drive/references/lark-drive-fetch.md` at line 53, Update the
`--token` + `--type` documentation row to remove `doc` from the listed aliases,
preserving only `sheets` → `sheet` and `base` → `bitable`; keep `docx` as the
distinct supported type.

Comment on lines +88 to +89
| 速览 / 理解 / 总结整份 PPT(只关注内容) | `drive +fetch --url "<原 URL>"` 直接读取 Markdown | [`lark-drive-fetch.md`](../lark-drive/references/lark-drive-fetch.md) |
| 读取指定页、检查版式 / 元素结构,或编辑前回读 | 解析 slides/wiki token,用 shortcut 回读全文 XML 或读取单页 XML,保存 `xml_presentation_id`、`slide_id`、`revision_id` | `slides +xml-get`、`xml_presentation.slide.get`、`lark-slides-xml-presentations-get.md` |

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 | 🟡 Minor | ⚡ Quick win

Identify xml_presentation.slide.get as a native API.

Line 89 calls both XML read paths a “shortcut”. slides +xml-get is a shortcut, but xml_presentation.slide.get is a native API. Update the wording so agents use the correct command form for single-slide reads.

🤖 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-slides/SKILL.md` around lines 88 - 89, Update the “读取指定页、检查版式 /
元素结构,或编辑前回读” guidance to distinguish `slides +xml-get` as the shortcut from
`xml_presentation.slide.get` as the native API, and specify the correct command
form for single-slide reads while preserving the existing token and ID
requirements.

return dec.Skip()
default:
// Unknown wrapper (including the synthetic contentroot): descend into children.
return r.renderChildren(dec, name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Do not silently flatten existing Docx content

Both docs +fetch --doc-format markdown for whole-document reads and drive +fetch now treat this renderer as successful, but it has no native <table> handler and readText discards nested elements and their attributes. For example, a table containing <a href="https://example.com">value</a> currently renders as just value\n, losing both the table structure and the link destination, so the document API fallback never runs. This is a silent fidelity regression from the previous Markdown path. Please keep the existing path until the renderer preserves the supported Docx XML semantics, or make the renderer fidelity-complete before treating this read as successful.

// --full reads may replace inline content with a local file descriptor.
func emitPaginatedMarkdown(runtime *common.RuntimeContext, content, title string, updateTime int64, hasMore bool, nextPageToken string) error {
nextPageToken = strings.TrimSpace(nextPageToken)
data := map[string]interface{}{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Preserve the existing docs fetch response contract

Whole-document Markdown reads previously forwarded the Docs API response, so existing consumers could read fields such as data.document.document_id, revision_id, and any returned reference_map, as well as data.tips. This synthetic response only keeps content, title, and update_time, so an existing command such as docs +fetch --doc-format markdown --jq ".data.document.document_id" now returns null. The bundled lark-doc-fetch.md contract still documents these fields. Please retain the existing metadata when adding pagination, or avoid switching the established command to a response shape that silently removes public fields.

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

Labels

domain/base PR touches the base domain domain/ccm PR touches the ccm domain size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants