Skip to content

feat(contact): add bot search shortcut - #2083

Open
sang-neo03 wants to merge 17 commits into
mainfrom
feat/search-bot
Open

feat(contact): add bot search shortcut#2083
sang-neo03 wants to merge 17 commits into
mainfrom
feat/search-bot

Conversation

@sang-neo03

@sang-neo03 sang-neo03 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add contact +search-bot for searching bots visible to the calling user with a user token. The shortcut validates keyword and filter inputs, preserves server notices, and returns stable agent-facing fields.

Changes

  • Register the contact +search-bot read shortcut with search:bot scope and user authentication.
  • Support keyword search, chat and chat-history filters, pagination, dry-run output, and human-readable output.
  • Parse and validate every --chat-ids value with the shared typed Chat ID validator, including URL normalization.
  • Validate --page-size in the API-supported range 1-30 and preserve opaque page tokens verbatim across real and dry-run requests.
  • Match +search-user output behavior: pretty uses the command summary while table, csv, and ndjson use the generic full-field formatter.
  • Parse bot names and descriptions from highlighted display_info while preserving match segments and bot metadata.
  • Document the command and identity-specific permission recovery in the lark-contact skill, remove a stale command example rejected by the manifest gate, add unit, dry-run E2E, and guarded live E2E coverage, and update the contact E2E coverage inventory.

Test Plan

  • make unit-test
  • go vet ./...
  • gofmt -l .
  • go mod tidy leaves go.mod and go.sum unchanged
  • go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=origin/main
  • make build && make quality-gate
  • node scripts/skill-format-check/index.js
  • go test -count=1 ./tests/cli_e2e/dryrun -run 'ContactSearchBot'
  • Manual local verification confirms lark-cli contact +search-bot is registered and absent from the application domain.

Related Issues

  • None

Summary by CodeRabbit

  • New Features
    • Added contact +search-bot to search the current user’s visible bots by keyword.
    • Supports optional narrowing with --chat-ids and --has-chatted, plus manual pagination via --page-size and --page-token.
    • Human-readable formats show a “next page” hint when more results exist; JSON/CSV/NDJSON include pagination fields without showing hints.
  • Documentation
    • Updated the Lark contact skill guide with +search-bot usage, returned fields, filters, and pagination flow.
  • Tests
    • Expanded unit, integration, dry-run, and CLI end-to-end coverage for validation, mapping, formatting, and paging behavior.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds the contact +search-bot shortcut with validation, filtered and paginated API requests, typed bot projection, formatted output, documentation, unit tests, and CLI end-to-end coverage.

Changes

Contact bot search

Layer / File(s) Summary
Command contract and request construction
shortcuts/contact/contact_search_bot.go, shortcuts/contact/shortcuts.go, shortcuts/contact/contact_search_bot_test.go
Defines and registers the shortcut, validates query and filter flags, constructs request bodies, and tests request validation and shapes.
API execution and output projection
shortcuts/contact/contact_search_bot.go, shortcuts/contact/contact_search_bot_test.go
Posts paginated searches, decodes and maps bot data, parses display highlights, renders formats, and tests response, pagination, dry-run, and typed errors.
Command documentation and end-to-end coverage
skills/lark-contact/SKILL.md, tests/cli_e2e/contact/contact_search_bot_workflow_test.go, tests/cli_e2e/dryrun/contact_search_bot_dryrun_test.go, tests/cli_e2e/contact/coverage.md
Documents bot search usage and verifies JSON workflow, dry-run request fields, and updated coverage metrics.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI
  participant ContactSearchBot
  participant BotSearchAPI
  User->>CLI: run contact +search-bot
  CLI->>ContactSearchBot: validate flags and build request
  ContactSearchBot->>BotSearchAPI: POST search with pagination
  BotSearchAPI-->>ContactSearchBot: return bot data and page metadata
  ContactSearchBot-->>CLI: render JSON or human-readable results
  CLI-->>User: display bots and pagination hint
Loading

Suggested labels: feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% 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 is concise and accurately summarizes the main change: adding a contact bot search shortcut.
Description check ✅ Passed The description follows the required template and includes Summary, Changes, Test Plan, and Related Issues sections.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/search-bot

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.

@github-actions github-actions Bot added domain/contact PR touches the contact domain size/L Large or sensitive change across domains or core paths labels Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add larksuite/cli#feat/search-bot -y -g

@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.

🧹 Nitpick comments (1)
shortcuts/contact/contact_search_bot.go (1)

160-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Chat-ids parsing/validation is duplicated between buildBotSearchBody and validateBotSearch.

Lines 165-174 re-implement the same CSV parse + "no valid chat_id parsed" check already performed in validateBotSearch (lines 134-140). Since buildBotSearchBody relies on Validate having already run (both in Execute and in DryRun), this duplicate check is currently dead weight, but if one copy is updated (e.g., a new chat-id constraint) and the other isn't, the two functions can silently diverge.

♻️ Suggested extraction
+func parseAndValidateChatIDs(raw string) ([]string, error) {
+	chatIDs := common.SplitCSV(raw)
+	if len(chatIDs) == 0 {
+		return nil, common.ValidationErrorf("--chat-ids: no valid chat_id parsed from %q (separate entries with ',')", raw).
+			WithParam("--chat-ids")
+	}
+	return chatIDs, nil
+}

Then call this helper from both validateBotSearch (adding the > maxBotSearchChatIDs check after) and buildBotSearchBody.

🤖 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/contact/contact_search_bot.go` around lines 160 - 184, Extract the
shared chat-IDs CSV parsing and empty-result validation from validateBotSearch
and buildBotSearchBody into a helper, then call it from both functions. Keep the
maxBotSearchChatIDs limit check in validateBotSearch after the helper returns,
and use the helper’s parsed IDs when populating filter.ChatIDs in
buildBotSearchBody.
🤖 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.

Nitpick comments:
In `@shortcuts/contact/contact_search_bot.go`:
- Around line 160-184: Extract the shared chat-IDs CSV parsing and empty-result
validation from validateBotSearch and buildBotSearchBody into a helper, then
call it from both functions. Keep the maxBotSearchChatIDs limit check in
validateBotSearch after the helper returns, and use the helper’s parsed IDs when
populating filter.ChatIDs in buildBotSearchBody.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e5770c0-de40-4525-90e9-a5be1964f151

📥 Commits

Reviewing files that changed from the base of the PR and between d2e22c5 and 705c5cd.

📒 Files selected for processing (6)
  • shortcuts/contact/contact_search_bot.go
  • shortcuts/contact/contact_search_bot_test.go
  • shortcuts/contact/shortcuts.go
  • skills/lark-contact/SKILL.md
  • tests/cli_e2e/contact/contact_search_bot_workflow_test.go
  • tests/cli_e2e/dryrun/contact_search_bot_dryrun_test.go

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.61584% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.22%. Comparing base (358cd06) to head (fcb661b).

Files with missing lines Patch % Lines
shortcuts/contact/contact_search_bot.go 91.38% 9 Missing and 9 partials ⚠️
shortcuts/contact/contact_search_bot_fanout.go 90.07% 8 Missing and 5 partials ⚠️
shortcuts/contact/shortcuts.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2083      +/-   ##
==========================================
+ Coverage   75.16%   75.22%   +0.05%     
==========================================
  Files         912      914       +2     
  Lines       96475    96816     +341     
==========================================
+ Hits        72517    72826     +309     
- Misses      18381    18399      +18     
- Partials     5577     5591      +14     

☔ View full report in Codecov by Harness.
📢 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.

parseBotDisplayInfo read the name from the first non-empty line but always
took the description from line 1. When line 0 is blank the two collapse: the
name is echoed back as its own description and the real description on the
next line is dropped. Track which line the name came from and read the
description from the line after it.

Also document the enable_join_group trap. The flag says a bot may be added to
chats, but doing so needs the app's cli_ app_id, which this command does not
return -- the ou_ open_id it returns is rejected by the chat-member APIs and
there is no open_id to app_id lookup. An agent reading the bare flag will
claim a bot was added when it cannot be. Called out in Tips and in the skill
doc, and the "open_id is usable downstream" wording is softened to say what
the id identifies rather than implying every downstream API accepts it.

The command Description listed keyword, chat and chat history as alternatives,
which reads as if a chat filter alone is a valid search; --query is required,
so the filters are now described as narrowing it.
Both contact/v3/users/search and bot/v4/bot/search accept user_id_type, and
union_id does come back as on_... on either. Neither +search-user nor
+search-bot exposes it, because both project the response into a fixed struct
whose field is named open_id -- switching the type would leave that field
holding a union_id or an employee id, so the name would lie. +get-user can
offer --user-id-type precisely because it passes the raw response through.

Record the rule and the escape hatch so the omission reads as a decision
rather than an oversight, and so the next reader does not add the flag without
also renaming the field.
Two spots diverged from the sibling command for no reason.

parseBotSearchChatIDs gated on Cmd.Flags().Changed("chat-ids"). +search-user
never does that for a string flag -- it tests emptiness
(strings.TrimSpace(runtime.Str(...)) != "") and reserves Changed() for bool
filters, where emptiness cannot distinguish unset from false. The practical
difference is that --chat-ids "" used to be an error here and a no-op there;
now a blank value reads as "no filter" in both, and only a non-blank value
that parses to zero entries is rejected. Tests pin both halves.

botSearchPagination existed only to return two runtime lookups to two callers.
+search-user builds its query params inline in DryRun and Execute, so do the
same and keep the opaque-page-token note where the token is actually read.
+search-user decodes page_token off the wire and deliberately never surfaces
it: no --page-token flag, no page_token in searchUserResponse, and a has_more
hint that tells the caller to refine instead ("add filters or tighten
--query"). Bot search had grown the opposite shape -- a --page-token flag, the
token in the envelope, and a hint pointing at it -- so the two sibling search
commands disagreed on whether pagination exists.

Align on the sibling: keep decoding page_token so the field is not silently
lost from the API type, but stop exposing it, and reword both the tip and the
stderr hint to name the narrowing options this command actually has
(--chat-ids, --has-chatted, a more specific --query).

TestBotSearchIntegrationEmptyPageTokenOmitted becomes
TestBotSearchIntegrationNeverSurfacesPageToken and now has the stub return a
token, so the test fails if the field is ever re-added to the envelope rather
than only covering the empty case.
+search-user has --queries: comma-separated keywords searched in parallel, a
flat users[] where every row carries matched_query, and a queries[] sidecar
holding each keyword's has_more and notice. Bot search had no equivalent, which
hurts more here than there because --query is mandatory, so resolving three bot
names meant three sequential invocations.

Mirror the sibling rather than invent a second mechanism: contact_search_bot_
fanout.go reuses parseAndDedupQueries, querySummary, fanoutConcurrency,
isFanoutSummaryFormat and the contactFanout* error helpers, and repeats its
structure -- one worker per keyword behind the same concurrency cap, panic
recovery per worker, results reindexed into query order, and failure only when
every keyword fails so a single bad keyword cannot sink the batch.

--chat-ids and --has-chatted narrow every keyword in the batch, matching how the
bool filters apply across the user fanout. --query and --queries are mutually
exclusive, and the "--query is required" rule is scoped to single-search mode so
it does not leak into fanout.
A consistency pass against +search-user turned up four real divergences.

The keyword error named only --query. Adding --queries made that a lie: an
agent reading param="--query" concludes --queries is not a way out. Switch to
WithParams naming both flags with a reason each, and fix the flag help, the tip
and the skill line that still said --query was unconditionally required.

p2p_chat_id carried omitempty while searchUser.P2PChatID does not, so the two
sibling commands disagreed on whether the key exists when there is no p2p chat.
Callers should not need a bot-specific presence check; drop omitempty and flip
the test to require the empty key.

affordance/contact.md had no +search-bot section, so its --help was missing the
When to use / Avoid when / Examples / Related skills blocks the other three
contact commands all have. Added, without a ### Tips block: affordance tips
replace the shortcut's own Tips rather than adding to them, which would have
hidden the fanout example and the enable_join_group warning.

--chat-ids validated the raw entry count against the 100 cap and then sent
duplicates through, while --user-ids is normalized and deduped by
common.resolveOpenIDs before its cap is checked. Duplicates spent the server's
array budget, and 101 copies of one chat were rejected here but accepted there.
Normalize, dedupe on the normalized value, then check the cap -- so a chat URL
and the bare id it contains collapse into one entry too.

Also reuse displayInfoHighlightRE instead of a second identical regex, and add
the fanout tests the sibling had and this one lacked: concurrency cap, panic
contained to one query with no stack trace leaking to stderr, and all-queries-
failed surfacing a typed error that keeps the upstream status.
Three leftovers from the review, all in guidance rather than request shaping.

--chat-ids and --has-chatted described themselves as narrowing "--query",
written before --queries existed. Both filters apply to every keyword in a
fanout, so the text told agents a combination they can use is unavailable, and
the skill doc contradicted itself: one paragraph demanded --query on every call,
another explained the filters applying across --queries. Both now say a keyword
search, and the skill line names either flag. Kept "narrow" over the sibling's
"restrict": these two filters genuinely cannot enumerate on their own.

The affordance section repeated the domain-level skill, so Related skills listed
lark-contact and lark-contact/SKILL.md -- the same file twice, since the merge
dedupes on the raw string. +search-user's second entry is a distinct per-command
reference; there is no such file here, so drop the block and let the domain
entry stand alone.

Validation reported the missing keyword before rejecting an explicit
--has-chatted=false, so a caller passing only that flag had to fix two errors in
sequence, the first of which was not what was actually wrong. +search-user lands
on the =false error first because it counts a Changed bool as search input; move
the check ahead of the keyword requirement to reach the same place, and pin the
order with a test.

Also add the fanout tests the sibling still had and this one did not: partial
failure keeping the surviving query's rows plus the stderr counters, ndjson
staying parseable with no summary line mixed in, and a cancelled context
short-circuiting every query before it reaches the transport.
Last round hoisted the explicit =false rejection to the top of validation to fix
which error a bare --has-chatted=false reports. That overshot: sitting ahead of
every keyword check, it also masked them. Measured against the sibling,
--query x --queries y --has-chatted=false returned --has-chatted here and the
mutual-exclusion error there, and a 51-character --query alongside =false
returned --has-chatted here and the length error there. Fixing one combination
had broken two others.

+search-user only reaches the =false error first when nothing else was supplied:
a Changed bool satisfies its "at least one search input" gate, then the keyword
checks no-op, then =false fires. Reproduce that shape instead of reordering
wholesale -- the =false error now returns from the no-keyword branch, and
otherwise stays after the keyword and chat-id checks. All four combinations now
report the same param, params and message as +search-user.

The partial-failure fanout test claimed to prove the surviving query's notice
reaches the caller but only checked that CSV stdout contained a keyword, which
comes from the row rather than the notice; deleting either notice assignment left
it green. Rewritten against JSON with explicit assertions on the top-level
notice, the surviving query's sidecar notice, the failed query's upstream status
and the single contributed row — verified by deleting the assignment and watching
it fail. The CSV summary it used to cover moved into its own test that also pins
the matched_query column.

Also add the fanout dry-run test: one previewed request per deduped keyword, each
carrying the filter, and no page_token in the preview.
…eference

Two things were wrong with how this branch edited skills/lark-contact/SKILL.md.

It changed content that has nothing to do with bot search. Commit 2830d5e
rewrote the user_profiles batch_query row in the command table, rewrote the
paragraph introducing it, and deleted its whole example block, redirecting
readers to lark-openapi-explorer. That was done to silence a local quality-gate
rejection -- "example references unknown command contact user_profiles
batch_query" -- which only fires because a stale internal/registry/meta_data.json
carries 13 services and no contact service. Refreshing the metadata makes the
gate pass with the documentation intact, so the removal deleted correct content
to work around a local artifact. Restored verbatim.

It also grew the bot search section inside the main skill file while the sibling
keeps that depth in references/lark-contact-search-user.md, so the section had
swollen past the surrounding material and still lacked the input and output
contract details the sibling documents. Added
references/lark-contact-search-bot.md covering the normalization rules
(--queries dedupe, --chat-ids URL normalization, both caps counted after
dedupe), the field contract, the fanout shape including the absent top-level
has_more and the per-query error, and which --format values put the fanout
counters on stderr. The main file keeps a short section that links to it, the
way the +search-user row already does.

Everything else in the file is now purely additive: the new table row, the new
section and two new caveat bullets, with the pre-existing 41050 and ID-type
bullets left untouched. The description front matter is the one edit that cannot
be an addition -- a skill has a single description and the capability has to
appear there for routing -- so it only inserts clauses and leaves every original
word in place.

Also corrects the pretty column count: the fanout table adds matched_query, so
it is seven columns, not the six the old line claimed unconditionally.
Eight places -- the command Description, the --chat-ids flag help, two tips, the
keyword-required error, the stderr hint, the affordance blurb and the reference
doc -- called --chat-ids a narrowing filter. Live probing shows it is not: a bot
inside a chat can be absent from the tenant-wide result set entirely, so naming
the chat widens what is reachable rather than filtering it down.

  query '红黑榜'  no filter -> 0 rows        with 3 eng chats -> 红黑榜小助手
  query '助'      no filter -> 2 rows        with 3 eng chats -> 红黑榜小助手

The second row is the point: the filtered result is not a subset, it is a
different set. --has-chatted by contrast is a real narrowing filter -- the same
'助' query drops from two rows to one, keeping the same open_id -- so wording
about narrowing now refers only to it.

This mattered beyond accuracy. An agent told --chat-ids "only narrows" would
never reach for it to *find* a bot a plain --query cannot see, which is the only
way to answer "what bot is in this chat". A tip now states that outright.

Also documents that keyword matching is not substring matching: '会议助手' finds
会议小助手 across an inserted character, yet '小助手' does not find 会议小助手 and
'红黑榜' does not find 红黑榜小助手, so a shorter fragment is not a safe fallback.

The reference doc gains what the +search-user reference has and it lacked: the
field contract as a table with types and per-field empty-value behaviour (three
distinct semantics -- omitted key, empty string, empty array), a
disambiguation section ranking description > has_chatted > is_agent since a
single keyword returns eight near-identically named bots, a dedicated fanout
section, and a table of rejected flag combinations with the param each one
names. Every row of that table and every remaining example was executed before
being written down.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/contact PR touches the contact 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