Skip to content

feat: support Base attachment APIs#887

Open
zgz2048 wants to merge 3 commits into
larksuite:mainfrom
zgz2048:codex/base-attachment-apis
Open

feat: support Base attachment APIs#887
zgz2048 wants to merge 3 commits into
larksuite:mainfrom
zgz2048:codex/base-attachment-apis

Conversation

@zgz2048
Copy link
Copy Markdown
Collaborator

@zgz2048 zgz2048 commented May 14, 2026

Summary

Refactor Base attachment handling onto the new attachment APIs and add first-class commands for downloading and removing Base record attachments. This improves agent ergonomics for multi-attachment workflows and avoids manual Base attachment download handling.

Changes

  • Refactored base +record-upload-attachment to upload one or more local files, send image width/height for PNG/JPEG/GIF uploads, and append uploaded file tokens through append_attachments.
  • Added base +record-download-attachment for Base attachment downloads by record-id, optionally filtered by one or more --file-token values; omitting --file-token downloads all attachments in the record.
  • Added base +record-remove-attachment for removing one or more attachment file tokens from a record attachment cell.
  • Updated Base attachment help text, dry-run coverage, lark-base skill guidance, and CLI tests.

Breaking Changes

  • base +record-upload-attachment --name is no longer supported; uploaded attachment names come from local file basenames.
  • base +record-upload-attachment now prints the append_attachments server data directly instead of older CLI-composed fields such as updated, uploaded, or attachment.
  • Upload writes now use append_attachments instead of reading the existing record and patching the whole attachment array.

Release Note

  • Base attachment upload now supports multiple files per command and preserves image preview ratio by sending image dimensions.
  • New Base attachment download command supports selected file tokens or whole-record attachment download.
  • New Base attachment remove command supports deleting one or more attachments from an attachment cell.
  • Deprecated upload --name support has been removed.

Test Plan

  • Unit tests pass: make unit-test
  • go vet ./...
  • gofmt -l .
  • go mod tidy left go.mod / go.sum unchanged
  • Dry-run E2E: go test ./tests/cli_e2e/base -run TestBase_AttachmentDryRun -count=1
  • Incremental lint against target branch: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=upstream/main
  • Manual PRE verification: uploaded a 160x90 PNG, downloaded it by whole-record and by file token, then removed it from the attachment cell

Note: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=origin/main currently reports pre-existing unrelated depguard/forbidigo issues in shortcuts/event, shortcuts/mail, shortcuts/sheets, shortcuts/minutes, and internal/keychain; the same lint against upstream/main reports 0 issues for this PR diff.

Related Issues

  • None

Summary by CodeRabbit

  • New Features

    • Append-only attachment flow: batch upload (up to 50 files, 2GB each), download (optional file-token, dedupe, collision checks), and bulk remove commands; --name flag deprecated/rejected; image dimension autodetection.
  • Documentation

    • Updated Base docs and command references to use explicit upload/download/remove attachment commands.
  • Tests

    • Expanded unit and e2e dry-run coverage and added tests for upload/download/remove behaviors and help guidance.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86cfba3e-fd63-4372-a905-be628ec674cc

📥 Commits

Reviewing files that changed from the base of the PR and between b1728c6 and 7d715ff.

📒 Files selected for processing (1)
  • shortcuts/base/record_upload_attachment.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • shortcuts/base/record_upload_attachment.go

📝 Walkthrough

Walkthrough

This PR replaces record-field PATCH-based attachment updates with Base v3 attachment endpoints: append_attachments and remove_attachments, and adds a dedicated download flow. It introduces upload/download/remove shortcuts (batch upload via repeatable --file), image-dimension detection, updated unit/execute/e2e dry-run tests, and documentation updates routing users to the new commands.

Changes

Base Attachment Operations Refactor

Layer / File(s) Summary
Command declarations & flags
shortcuts/base/record_upload_attachment.go
Added BaseRecordDownloadAttachment and BaseRecordRemoveAttachment. Changed BaseRecordUploadAttachment --file to repeatable string_array (batch up to 50, 2GB per file); --name hidden/deprecated. Wired flags, tips, validate/execute/dry-run handlers.
Dry-run orchestration & validation
shortcuts/base/record_upload_attachment.go, shortcuts/base/base_dryrun_ops_test.go, tests/cli_e2e/base/base_attachment_dryrun_test.go
Dry-run paths updated to model append_attachments / get_attachments / remove_attachments. Validation rejects --name, normalizes file/token arrays, and enforces --output directory constraints when downloading multiple/all. Dry-run e2e asserts API URLs and request-body shapes.
Upload execution path
shortcuts/base/record_upload_attachment.go, shortcuts/base/base_execute_test.go
Upload now: resolve/validate attachment field, validate local files, upload media (multipart when >20MB), map uploads to file_token items with optional image_width/image_height, and POST a nested single-cell append_attachments payload. Tests updated to generate PNGs and assert append payloads and returned tokens.
Download execution path & planning
shortcuts/base/record_upload_attachment.go, shortcuts/base/base_execute_test.go
New download command: fetch metadata via get_attachments, dedupe by file_token, plan output paths (collision/disambiguation/overwrite rules), stream-download via Drive media endpoint, and report structured progress/errors. Tests cover extra_info propagation, download-all behavior, deduplication, conflict preflight, and failure reporting.
Remove execution path
shortcuts/base/record_upload_attachment.go, shortcuts/base/base_execute_test.go
New remove command: normalize --file-token inputs, validate attachment field type, build single-cell remove_attachments body containing tokens, and POST removal. Tests assert removal payload shapes and stdout behavior.
Helpers & payload builders
shortcuts/base/record_upload_attachment.go, shortcuts/base/record_upload_attachment_test.go
Added helpers: detectAttachmentImageDimensions (image decode config), payload builders for nested single-cell attachments shape, dedupe and download planning helpers. New unit test TestDetectAttachmentImageDimensions.
Shortcut catalog & help tests
shortcuts/base/shortcuts.go, shortcuts/base/base_shortcuts_test.go
Inserted new shortcuts into the catalog; added BaseRecordRemoveAttachment to delete-risk checks; added TestBaseAttachmentHelpGuidesAgents to validate flag help and CLI tips for upload/download/remove.
Dry-run E2E coverage
tests/cli_e2e/base/base_attachment_dryrun_test.go, tests/cli_e2e/base/coverage.md
Added TestBase_AttachmentDryRun exercising upload, download-single, download-all, and remove dry-run flows with stdout-based API URL/body assertions; coverage table updated to mark attachment commands as tested (dry-run only).
Docs & references
skills/lark-base/SKILL.md, skills/lark-base/references/*
Rewrote guidance to use dedicated Base attachment commands (+record-upload-attachment, +record-download-attachment, +record-remove-attachment), removed old upload doc content, and updated cell-value/record docs to reflect append/remove/download semantics and required flags.
Tests: execute & unit updates
shortcuts/base/base_execute_test.go, shortcuts/base/base_dryrun_ops_test.go, shortcuts/base/record_upload_attachment_test.go
Updated small/large upload execute tests to expect append_attachments POSTs and token markers; added tests rejecting deprecated --name, multiple download scenarios, remove behavior tests, and TestDetectAttachmentImageDimensions unit test.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant BaseAPI as Base v3 API
    participant DriveAPI as Drive API
    participant FileSystem

    rect rgba(100, 200, 255, 0.5)
    Note over Client,FileSystem: Upload Attachment (New Flow)
    Client->>FileSystem: Read local files
    FileSystem-->>Client: File bytes
    Client->>BaseAPI: POST /open-apis/base/v3/.../upload_file (multipart if >20MB)
    BaseAPI-->>Client: file_token
    Client->>BaseAPI: POST /open-apis/base/v3/.../append_attachments {rec_id, field_id, file_token, image_width?, image_height?}
    BaseAPI-->>Client: Success
    end

    rect rgba(100, 255, 200, 0.5)
    Note over Client,FileSystem: Download Attachment (New Flow)
    Client->>BaseAPI: GET /open-apis/base/v3/.../get_attachments (optional file_token filter)
    BaseAPI-->>Client: Attachment metadata list
    Client->>DriveAPI: GET /drive/v1/medias/{file_token}/download
    DriveAPI-->>FileSystem: Stream file bytes
    Client->>FileSystem: Save to --output directory
    FileSystem-->>Client: Local file path
    end

    rect rgba(255, 200, 100, 0.5)
    Note over Client,BaseAPI: Remove Attachment (New Flow)
    Client->>BaseAPI: POST /open-apis/base/v3/.../remove_attachments {rec_id, field_id, file_tokens: [...]}
    BaseAPI-->>Client: Success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • larksuite/cli#441: Related attachment upload/multipart changes touching the same upload path and tests.
  • larksuite/cli#404: Prior doc/guidance around attachment download routing; replaced by the new download shortcut flow.
  • larksuite/cli#563: Similar changes to upload payload/test expectations for attachment uploads.

Suggested reviewers

  • kongenpei
  • timzhong1024

"I hopped through code with carrot-bright eyes,
Append, download, and remove — no more surprise,
Tokens in paw, dimensions in tow,
The Base now handles attachments like whoa,
A rabbit cheers as tests all rise!" 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.56% 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: adding Base attachment API support through new commands for upload, download, and remove operations.
Description check ✅ Passed The description follows the template structure with all required sections: Summary, Changes, Test Plan (with checkboxes), and Related Issues. All key information is present and comprehensive.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@github-actions github-actions Bot added domain/base PR touches the base domain size/L Large or sensitive change across domains or core paths labels May 14, 2026
@zgz2048 zgz2048 marked this pull request as ready for review May 14, 2026 09:53
@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

❌ Patch coverage is 73.27394% with 120 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.91%. Comparing base (37459b6) to head (7d715ff).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
shortcuts/base/record_upload_attachment.go 73.15% 81 Missing and 39 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #887      +/-   ##
==========================================
+ Coverage   65.89%   65.91%   +0.01%     
==========================================
  Files         518      520       +2     
  Lines       48945    49539     +594     
==========================================
+ Hits        32254    32652     +398     
- Misses      13921    14083     +162     
- Partials     2770     2804      +34     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/base/base_execute_test.go`:
- Line 1968: The test name mismatches its assertions: Test currently named
"download attachment uses extra info" but asserts the returned map lacks
extra_info_used because downloadBaseAttachment never returns that key; either
rename the test to reflect what it actually verifies (e.g., "download attachment
includes extra parameter in request" /
TestDownloadAttachment_IncludesExtraParameterInRequest) or change the
implementation to include extra_info_used in downloadBaseAttachment's returned
map; update the test function name and any references accordingly or add the
extra_info_used field to the return value and adjust assertions to expect it.
🪄 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

Run ID: 3d05e80e-0681-499d-8c5e-52c95bff2812

📥 Commits

Reviewing files that changed from the base of the PR and between a54a879 and 77bc0be.

📒 Files selected for processing (12)
  • shortcuts/base/base_dryrun_ops_test.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/base/base_shortcuts_test.go
  • shortcuts/base/record_upload_attachment.go
  • shortcuts/base/record_upload_attachment_test.go
  • shortcuts/base/shortcuts.go
  • skills/lark-base/SKILL.md
  • skills/lark-base/references/lark-base-cell-value.md
  • skills/lark-base/references/lark-base-record-upload-attachment.md
  • skills/lark-base/references/lark-base-record.md
  • tests/cli_e2e/base/base_attachment_dryrun_test.go
  • tests/cli_e2e/base/coverage.md
💤 Files with no reviewable changes (1)
  • skills/lark-base/references/lark-base-record-upload-attachment.md

t.Fatalf("downloaded=%#v", data["downloaded"])
}
got, _ := gotItems[0].(map[string]interface{})
if got["file_token"] != "box_a" || got["saved_path"] == "" || got["extra_info_used"] != 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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, locate the test file
find . -name "base_execute_test.go" -type f

Repository: larksuite/cli

Length of output: 95


🏁 Script executed:

# Read the test function around line 1968 with context
# Start from line 1900 to see the full test setup
head -1971 shortcuts/base/base_execute_test.go | tail -100

Repository: larksuite/cli

Length of output: 3127


🏁 Script executed:

# Also check the test function name around line 1968
sed -n '1900,1980p' shortcuts/base/base_execute_test.go | head -100

Repository: larksuite/cli

Length of output: 2605


🏁 Script executed:

rg "extra_info_used" --type go -B 2 -A 2

Repository: larksuite/cli

Length of output: 430


🏁 Script executed:

# Also search for where this output field is set in the implementation
rg "extra_info_used" -A 3 -B 3

Repository: larksuite/cli

Length of output: 556


🏁 Script executed:

# Search for where extra_info_used is set in the actual implementation
fd "base_execute" --type f -not -name "*test.go" | xargs rg "extra_info_used" -B 5 -A 5

Repository: larksuite/cli

Length of output: 1112


🏁 Script executed:

# Let's look at the base_execute.go file to find the download implementation
fd "base_execute.go" --type f -not -name "*test.go"

Repository: larksuite/cli

Length of output: 288


🏁 Script executed:

# Find base_execute.go (the implementation, not the test)
find . -name "base_execute.go" -type f | grep -v test

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Search for extra_info_used in non-test files
rg "extra_info_used" --type go --glob '!*test.go' -B 5 -A 5

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Find the base_execute.go implementation file
find . -name "base_execute.go" -type f

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Search for "downloaded" output structure to understand what fields are returned
rg "downloaded" --type go --glob '!*test.go' -B 3 -A 3 | head -50

Repository: larksuite/cli

Length of output: 3746


🏁 Script executed:

# Search for where saved_path is set in the output
rg "saved_path" --type go --glob '!*test.go' -B 3 -A 3

Repository: larksuite/cli

Length of output: 6905


🏁 Script executed:

# Let's also search for the structure that gets serialized to the "downloaded" output
rg "file_token.*saved_path" --type go -B 5 -A 5

Repository: larksuite/cli

Length of output: 863


🏁 Script executed:

# Search for the record_download_attachment implementation
rg "record_download_attachment\|RecordDownloadAttachment" --type go --glob '!*test.go' -l

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Let's look for record_download in base files
find shortcuts/base -name "*.go" -type f ! -name "*test.go" | xargs ls -la

Repository: larksuite/cli

Length of output: 7507


🏁 Script executed:

# Search for BaseRecordDownloadAttachment definition
rg "BaseRecordDownloadAttachment" --type go --glob '!*test.go' -B 3 -A 10

Repository: larksuite/cli

Length of output: 1805


🏁 Script executed:

# Check the record_upload_attachment.go for download implementation or clues
head -100 shortcuts/base/record_upload_attachment.go

Repository: larksuite/cli

Length of output: 4272


🏁 Script executed:

# Search for executeRecordDownloadAttachment implementation
rg "executeRecordDownloadAttachment" --type go --glob '!*test.go' -A 100 | head -150

Repository: larksuite/cli

Length of output: 12370


🏁 Script executed:

# Search for downloadBaseAttachment function to see what it returns
rg "func downloadBaseAttachment" --type go -A 80 | head -120

Repository: larksuite/cli

Length of output: 6301


🏁 Script executed:

# Let's check if there are any other tests that may use extra_info_used or show expected behavior
rg "extra_info" --type go | grep -v Binary

Repository: larksuite/cli

Length of output: 605


🏁 Script executed:

# Let's check the full implementation of downloadBaseAttachment to understand the complete return structure
grep -A 50 "func downloadBaseAttachment" shortcuts/base/record_upload_attachment.go | tail -40

Repository: larksuite/cli

Length of output: 1445


🏁 Script executed:

# Let's verify by looking at the complete return value one more time to be absolutely certain
sed -n '600,700p' shortcuts/base/record_upload_attachment.go | grep -A 15 "return map"

Repository: larksuite/cli

Length of output: 505


Test name doesn't match assertion behavior.

The test is named "download attachment uses extra info" and correctly verifies that the download request includes the extra query parameter. However, the assertion got["extra_info_used"] != nil expects this field to be absent (nil) from the output, which contradicts the test name's implication that the output reflects extra info usage.

The implementation confirms that downloadBaseAttachment never includes extra_info_used in its returned map (returns only: record_id, field_id, file_token, name, size, saved_path, size_bytes, content_type), so the assertion is technically correct. But the test name should clarify that it only validates the request includes the extra parameter, not that the output documents extra info usage. Consider renaming to "download attachment includes extra parameter in request" or add extra_info_used to the output if tracking this was the intent.

🤖 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/base/base_execute_test.go` at line 1968, The test name mismatches
its assertions: Test currently named "download attachment uses extra info" but
asserts the returned map lacks extra_info_used because downloadBaseAttachment
never returns that key; either rename the test to reflect what it actually
verifies (e.g., "download attachment includes extra parameter in request" /
TestDownloadAttachment_IncludesExtraParameterInRequest) or change the
implementation to include extra_info_used in downloadBaseAttachment's returned
map; update the test function name and any references accordingly or add the
extra_info_used field to the return value and adjust assertions to expect it.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 14, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@7d715ff932173123c0d4e8d3d15f7b2399fd5162

🧩 Skill update

npx skills add zgz2048/cli#codex/base-attachment-apis -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
shortcuts/base/record_upload_attachment.go (5)

770-784: ⚡ Quick win

Move the existence pre-check before DoAPIStream to avoid wasted API calls.

The !overwrite existence check at Lines 780–784 happens after the download HTTP request has already been initiated via DoAPIStream. When the target file already exists and --overwrite is false, this causes an avoidable network round-trip, consumes API quota, and burns rate-limit budget — only to fail validation right after.

Note that planAttachmentDownloadTargets (Lines 695–699) already performs the same existence check at plan time, so the post-API repetition appears to be defensive against a TOCTOU between plan and execute. In that case, the re-check still has value, but it should run before the API call.

♻️ Suggested reordering
 func downloadBaseAttachment(ctx context.Context, runtime *common.RuntimeContext, item baseAttachmentDownloadItem, targetPath string, overwrite bool) (map[string]interface{}, error) {
 	if _, err := runtime.ResolveSavePath(targetPath); err != nil {
 		return nil, output.ErrValidation("unsafe output path: %s", err)
 	}
 
+	if !overwrite {
+		if _, statErr := runtime.FileIO().Stat(targetPath); statErr == nil {
+			return nil, output.ErrValidation("output file already exists: %s (use --overwrite to replace)", targetPath)
+		}
+	}
+
 	query := larkcore.QueryParams{}
 	if item.ExtraInfo != "" {
 		query.Set("extra", item.ExtraInfo)
 	}
 	resp, err := runtime.DoAPIStream(ctx, &larkcore.ApiReq{
 		HttpMethod:  http.MethodGet,
 		ApiPath:     fmt.Sprintf("/open-apis/drive/v1/medias/%s/download", validate.EncodePathSegment(item.FileToken)),
 		QueryParams: query,
 	})
 	if err != nil {
 		return nil, output.ErrNetwork("download failed: %v", err)
 	}
 	defer resp.Body.Close()
-
-	if !overwrite {
-		if _, statErr := runtime.FileIO().Stat(targetPath); statErr == nil {
-			return nil, output.ErrValidation("output file already exists: %s (use --overwrite to replace)", targetPath)
-		}
-	}
 	result, err := runtime.FileIO().Save(targetPath, fileio.SaveOptions{
🤖 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/base/record_upload_attachment.go` around lines 770 - 784, The
post-request file-existence check should be moved before initiating the download
to avoid wasted API calls: before calling runtime.DoAPIStream(...) (the current
ApiReq/DoAPIStream block), perform the same !overwrite check using
runtime.FileIO().Stat(targetPath) and return output.ErrValidation(...) if the
file exists; only after that check succeeds should you call runtime.DoAPIStream
to create resp and defer resp.Body.Close(); keep the same error types and
messages and preserve the defensive TOCTOU protection by checking immediately
prior to the network call.

601-675: 💤 Low value

Same file_token across multiple attachment fields silently drops all but the first occurrence.

In Base it is valid for the same file_token to appear in more than one attachment field of the same record. The dedup at Lines 631–633 keeps only the first occurrence (in sorted currentFieldID order), so the FieldID reported back to the user in the download summary is whichever field sorted first — not necessarily the one the user expected.

For the "download by --file-token" path this only affects the reported field_id in the JSON envelope (the bytes are the same), so it's not a correctness bug, but users tracking attachments by (record, field) pairs may be confused. Worth a docstring on selectAttachmentDownloadItems or a tip in the shortcut help noting that cross-field duplicates resolve to a single download keyed by the sorted-first field.

🤖 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/base/record_upload_attachment.go` around lines 601 - 675,
selectAttachmentDownloadItems currently deduplicates byToken which silently
drops duplicate file_token occurrences across different fields; update the
function to preserve all occurrences by changing byToken from
map[string]baseAttachmentDownloadItem to map[string][]baseAttachmentDownloadItem
(or similar) so you append each baseAttachmentDownloadItem for a given fileToken
instead of skipping when exists, then adjust the downstream assembly: when
tokens is empty, flatten all slices into result; when tokens are provided, look
up the slice for each token and append all items (error only if token key
missing); keep sorting but include FieldID (or FileToken+FieldID) in the
tie-breaker to produce deterministic ordering; update references to byToken, the
lookup branch that returned a single item, and any sorting comparator in
selectAttachmentDownloadItems accordingly.

233-245: 💤 Low value

Possible nil deref guarded only by short-circuit — make the intent explicit.

info.IsDir() at Line 240 is only safe because statErr != nil short-circuits the ||. This works in practice (well-behaved Stat implementations don't return (nil, nil)), but readers must mentally trace the short-circuit to confirm safety, and a future change that reorders the operands would dereference a nil interface.

Consider making the nil-safety explicit:

-		info, statErr := runtime.FileIO().Stat(runtime.Str("output"))
-		if statErr != nil || !info.IsDir() {
+		info, statErr := runtime.FileIO().Stat(runtime.Str("output"))
+		if statErr != nil || info == nil || !info.IsDir() {
 			return common.FlagErrorf("--output must be an existing directory when downloading multiple attachments or when --file-token is omitted")
 		}
🤖 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/base/record_upload_attachment.go` around lines 233 - 245, In
validateRecordDownloadAttachment, make the nil-check for the FileInfo returned
by runtime.FileIO().Stat explicit before calling info.IsDir(): call info,
statErr := runtime.FileIO().Stat(runtime.Str("output")), then test if statErr !=
nil || info == nil || !info.IsDir() to decide returning
common.FlagErrorf("--output must be an existing directory when downloading
multiple attachments or when --file-token is omitted"); this ensures info is not
nil before invoking info.IsDir() and preserves the original behavior.

270-294: 🏗️ Heavy lift

Partial-failure on multi-file upload leaks already-uploaded media.

The loop uploads each file to Base media first, then a single append_attachments call links them all at the end. If file N fails to upload, files 1…N-1 are already on the server but never linked to any record — they become orphaned media. Retrying will re-upload them, creating duplicate orphans on every attempt.

This is a real concern for the multi-file workflow this PR is enabling. Consider one of:

  • Calling append_attachments incrementally per successful upload (more API calls, but no orphans).
  • Surfacing the orphaned file_tokens in the error detail so users (or follow-up tooling) can clean up or re-link them.

At minimum, the error returned at Line 283 should include the file_tokens of the already-uploaded files so the user isn't blind.

🤖 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/base/record_upload_attachment.go` around lines 270 - 294, The loop
that uploads files (see appendItems and uploadAttachmentToBase) can leave
already-uploaded media orphaned if a later upload fails; modify the error path
inside the loop so that when uploadAttachmentToBase returns an error you collect
and return the file_token(s) from the attachments already in appendItems (or
from the returned attachment objects) in the error message/return value (e.g.,
extract file_token fields from attachmentAppendItem/attachment objects and
include them in the error), or alternatively switch to calling
append_attachments incrementally after each successful upload (i.e., call
buildSingleCellAttachmentsBody + baseV3Call per uploaded file) to avoid orphans
entirely; update the error returned where uploadAttachmentToBase fails to
include the list of uploaded file_token strings so callers can clean up.

395-419: 💤 Low value

Drop normalizeOptionalAttachmentFileTokens as it has no callers.

normalizeOptionalAttachmentFileTokens (lines 395–400) is unused in the codebase and delegates to normalizeAttachmentFileTokens, which errors on duplicates. Meanwhile, normalizeOptionalDownloadAttachmentFileTokens (lines 402–419) silently dedupes via dedupeStringsPreserveOrder. The divergent duplicate semantics and unused helper create unnecessary maintenance burden.

🤖 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/base/record_upload_attachment.go` around lines 395 - 419, Remove
the dead wrapper function normalizeOptionalAttachmentFileTokens: delete the
normalizeOptionalAttachmentFileTokens definition since it has no callers and
only delegates to normalizeAttachmentFileTokens; before removing, search the
repo for any references to normalizeOptionalAttachmentFileTokens and update them
to call normalizeAttachmentFileTokens or
normalizeOptionalDownloadAttachmentFileTokens as appropriate, and ensure
duplicate-handling semantics remain intentional between
normalizeAttachmentFileTokens (which errors on duplicates) and
normalizeOptionalDownloadAttachmentFileTokens (which dedupes) so behavior is not
accidentally changed.
🤖 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/base/record_upload_attachment.go`:
- Around line 506-511: detectAttachmentImageDimensions fails to decode WebP
because only PNG/JPEG/GIF decoders are registered, causing the misleading
warning in the branch that checks strings.HasPrefix(mimeType, "image/"); either
register the WebP decoder by adding the blank import _ "golang.org/x/image/webp"
alongside the existing blank imports (lines where image decoders are registered)
so detectAttachmentImageDimensions can return real dimensions, or change the
warning condition in the code that assigns
attachment["image_width"]/["image_height"] (the if/else block using
detectAttachmentImageDimensions) to only print the warning for the specific mime
types you actually decode (e.g., "image/png","image/jpeg","image/gif") instead
of all image/* MIME types.

---

Nitpick comments:
In `@shortcuts/base/record_upload_attachment.go`:
- Around line 770-784: The post-request file-existence check should be moved
before initiating the download to avoid wasted API calls: before calling
runtime.DoAPIStream(...) (the current ApiReq/DoAPIStream block), perform the
same !overwrite check using runtime.FileIO().Stat(targetPath) and return
output.ErrValidation(...) if the file exists; only after that check succeeds
should you call runtime.DoAPIStream to create resp and defer resp.Body.Close();
keep the same error types and messages and preserve the defensive TOCTOU
protection by checking immediately prior to the network call.
- Around line 601-675: selectAttachmentDownloadItems currently deduplicates
byToken which silently drops duplicate file_token occurrences across different
fields; update the function to preserve all occurrences by changing byToken from
map[string]baseAttachmentDownloadItem to map[string][]baseAttachmentDownloadItem
(or similar) so you append each baseAttachmentDownloadItem for a given fileToken
instead of skipping when exists, then adjust the downstream assembly: when
tokens is empty, flatten all slices into result; when tokens are provided, look
up the slice for each token and append all items (error only if token key
missing); keep sorting but include FieldID (or FileToken+FieldID) in the
tie-breaker to produce deterministic ordering; update references to byToken, the
lookup branch that returned a single item, and any sorting comparator in
selectAttachmentDownloadItems accordingly.
- Around line 233-245: In validateRecordDownloadAttachment, make the nil-check
for the FileInfo returned by runtime.FileIO().Stat explicit before calling
info.IsDir(): call info, statErr :=
runtime.FileIO().Stat(runtime.Str("output")), then test if statErr != nil ||
info == nil || !info.IsDir() to decide returning common.FlagErrorf("--output
must be an existing directory when downloading multiple attachments or when
--file-token is omitted"); this ensures info is not nil before invoking
info.IsDir() and preserves the original behavior.
- Around line 270-294: The loop that uploads files (see appendItems and
uploadAttachmentToBase) can leave already-uploaded media orphaned if a later
upload fails; modify the error path inside the loop so that when
uploadAttachmentToBase returns an error you collect and return the file_token(s)
from the attachments already in appendItems (or from the returned attachment
objects) in the error message/return value (e.g., extract file_token fields from
attachmentAppendItem/attachment objects and include them in the error), or
alternatively switch to calling append_attachments incrementally after each
successful upload (i.e., call buildSingleCellAttachmentsBody + baseV3Call per
uploaded file) to avoid orphans entirely; update the error returned where
uploadAttachmentToBase fails to include the list of uploaded file_token strings
so callers can clean up.
- Around line 395-419: Remove the dead wrapper function
normalizeOptionalAttachmentFileTokens: delete the
normalizeOptionalAttachmentFileTokens definition since it has no callers and
only delegates to normalizeAttachmentFileTokens; before removing, search the
repo for any references to normalizeOptionalAttachmentFileTokens and update them
to call normalizeAttachmentFileTokens or
normalizeOptionalDownloadAttachmentFileTokens as appropriate, and ensure
duplicate-handling semantics remain intentional between
normalizeAttachmentFileTokens (which errors on duplicates) and
normalizeOptionalDownloadAttachmentFileTokens (which dedupes) so behavior is not
accidentally changed.
🪄 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

Run ID: 80f44a1e-3279-4e26-b754-5276485d097a

📥 Commits

Reviewing files that changed from the base of the PR and between 77bc0be and b1728c6.

📒 Files selected for processing (2)
  • shortcuts/base/base_execute_test.go
  • shortcuts/base/record_upload_attachment.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • shortcuts/base/base_execute_test.go

Comment on lines +506 to 511
if width, height, ok := detectAttachmentImageDimensions(runtime.FileIO(), filePath, mimeType); ok {
attachment["image_width"] = width
attachment["image_height"] = height
} else if strings.HasPrefix(mimeType, "image/") {
fmt.Fprintf(runtime.IO().ErrOut, "Warning: image dimensions unavailable for %s; attachment may display as square\n", fileName)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

WebP attachments will always trigger the "image dimensions unavailable" warning.

detectAttachmentMIMEFromContent recognizes image/webp (Lines 882–884), but the blank imports at Lines 11–14 only register PNG/JPEG/GIF decoders for image.DecodeConfig. As a result, any WebP upload reaches detectAttachmentImageDimensions, fails decoding, and then hits the strings.HasPrefix(mimeType, "image/") branch — printing a misleading warning to stderr saying the file "may display as square" even though WebP is a valid image format the server can render with proper dimensions.

Either add _ "golang.org/x/image/webp" (if that dependency is acceptable) so dimensions are detected, or scope the warning to only the formats we actually attempt to decode.

🛡️ Suggested scoping of the warning (no new dep)
 	if width, height, ok := detectAttachmentImageDimensions(runtime.FileIO(), filePath, mimeType); ok {
 		attachment["image_width"] = width
 		attachment["image_height"] = height
-	} else if strings.HasPrefix(mimeType, "image/") {
+	} else if mimeType == "image/png" || mimeType == "image/jpeg" || mimeType == "image/gif" {
 		fmt.Fprintf(runtime.IO().ErrOut, "Warning: image dimensions unavailable for %s; attachment may display as square\n", fileName)
 	}
📝 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 width, height, ok := detectAttachmentImageDimensions(runtime.FileIO(), filePath, mimeType); ok {
attachment["image_width"] = width
attachment["image_height"] = height
} else if strings.HasPrefix(mimeType, "image/") {
fmt.Fprintf(runtime.IO().ErrOut, "Warning: image dimensions unavailable for %s; attachment may display as square\n", fileName)
}
if width, height, ok := detectAttachmentImageDimensions(runtime.FileIO(), filePath, mimeType); ok {
attachment["image_width"] = width
attachment["image_height"] = height
} else if mimeType == "image/png" || mimeType == "image/jpeg" || mimeType == "image/gif" {
fmt.Fprintf(runtime.IO().ErrOut, "Warning: image dimensions unavailable for %s; attachment may display as square\n", fileName)
}
🤖 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/base/record_upload_attachment.go` around lines 506 - 511,
detectAttachmentImageDimensions fails to decode WebP because only PNG/JPEG/GIF
decoders are registered, causing the misleading warning in the branch that
checks strings.HasPrefix(mimeType, "image/"); either register the WebP decoder
by adding the blank import _ "golang.org/x/image/webp" alongside the existing
blank imports (lines where image decoders are registered) so
detectAttachmentImageDimensions can return real dimensions, or change the
warning condition in the code that assigns
attachment["image_width"]/["image_height"] (the if/else block using
detectAttachmentImageDimensions) to only print the warning for the specific mime
types you actually decode (e.g., "image/png","image/jpeg","image/gif") instead
of all image/* MIME types.

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 size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant