Skip to content

Feature/claude code bot#407

Open
richardiitse wants to merge 19 commits intolarksuite:mainfrom
richardiitse:feature/claude-code-bot
Open

Feature/claude code bot#407
richardiitse wants to merge 19 commits intolarksuite:mainfrom
richardiitse:feature/claude-code-bot

Conversation

@richardiitse
Copy link
Copy Markdown

@richardiitse richardiitse commented Apr 10, 2026

Summary

This PR adds a new lark-cli bot command that integrates Claude Code with Feishu/Lark, enabling users to chat with Claude Code directly from Feishu messages.

Changes

New Commands

  • lark-cli bot start - Start the Claude Code Bot
  • lark-cli bot status - View Bot status
  • lark-cli bot stop - Stop running Bot

Core Modules

  • Event subscription (shortcuts/bot/subscribe.go) - WebSocket-based Lark event listener
  • Message handling (shortcuts/bot/handler.go) - Event parsing and Claude routing
  • Claude integration (shortcuts/bot/claude.go) - CLI invocation with JSON parsing
  • Session management (shortcuts/bot/session.go) - TTL-based persistence with atomic writes
  • Command routing (shortcuts/bot/router.go) - Slash commands and pattern matching
  • Message sending (shortcuts/bot/sender.go) - Real Lark IM API integration

Documentation

  • README.bot.md - Bot feature guide
  • cmd/bot/TEST.md - Testing guide
  • docs/bot-integration-plan.md - Technical design
  • docs/CODEMAPS/ - Architecture, backend, and dependency codemaps

Test Coverage

  • Unit tests pass with 80.2% coverage for shortcuts/bot package
  • All bot module tests green
  • Code compiles successfully

Test Plan

  • Unit tests pass (80%+ coverage for shortcuts/bot)
  • Code compiles successfully
  • All bot module tests green

🤖 Generated with Claude Code

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 10, 2026

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions bot added the size/L Large or sensitive change across domains or core paths label Apr 10, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

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 a new bot subcommand with start/status/stop controls and implements a bot subsystem (Claude CLI client, event subscriber, handler, router, session manager, sender). Integrates the command into the root CLI and adds multiple documentation and generated report artifacts describing architecture, dependencies, tests, and an integration plan.

Changes

Cohort / File(s) Summary
Bot CLI commands
cmd/bot/bot.go, cmd/bot/start.go, cmd/bot/status.go, cmd/bot/stop.go
Adds bot Cobra command plus start/status/stop subcommands; introduces BotOptions and per-command options. start wires event subscription and blocks; status/stop currently return placeholder not_implemented JSON.
Event subscription & runtime
shortcuts/bot/subscribe.go, shortcuts/bot/sender.go
Implements EventSubscriber to connect via WebSocket, route im.message.receive_v1 events to handler, and send replies; MessageSender is a stubbed sender (TODO).
Claude integration
shortcuts/bot/claude.go, shortcuts/bot/claude_test.go
Adds Claude CLI wrapper with validation, retry/backoff, JSON parsing of CLI output, and unit tests covering defaults, retryability, unmarshalling, and cancellation.
Handler, routing & sessions
shortcuts/bot/handler.go, shortcuts/bot/router.go, shortcuts/bot/session.go, shortcuts/bot/*_test.go
Adds BotHandler for event parsing and Claude invocation, command/pattern routers (whitelist/alias/regex priority), TTL-backed SessionManager with atomic persistence and cleanup, and extensive unit tests for handler, router, and session behaviors.
CLI integration
cmd/root.go
Registers the new bot subcommand on the root command.
Docs & plans
README.bot.md, cmd/bot/TEST.md, docs/CODEMAPS/architecture.md, docs/CODEMAPS/backend.md, docs/CODEMAPS/dependencies.md, docs/bot-integration-plan.md, .reports/codemap-diff.txt
Adds Chinese README for bot, a bot testing guide, multiple CODEMAP docs describing architecture/backend/dependencies, a bot integration plan, and an auto-generated codemap report.
Repo metadata
.gitignore
Removes docs/ from one ignore block (minor change).

Sequence Diagram(s)

sequenceDiagram
    participant Feishu as Feishu (Platform)
    participant Subscriber as EventSubscriber
    participant Handler as BotHandler/Router/SessionMgr
    participant Claude as Claude CLI
    participant Sender as MessageSender

    Feishu->>Subscriber: WebSocket event (im.message.receive_v1)
    Subscriber->>Handler: parse event, extract text
    Handler->>Handler: lookup/create session (SessionManager)
    Handler->>Claude: ProcessMessage (claude CLI)
    Claude-->>Handler: JSON result (result, session_id)
    Handler->>Handler: update session state
    Handler->>Sender: send reply payload
    Sender->>Feishu: create message (reply)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

domain/im

Suggested reviewers

  • tuxedomm
  • fangshuyu-768

Poem

🐰 I hopped through docs and code tonight,
A bot awoke and parsed the light,
Sessions kept snug, routers in play,
Claude spoke JSON, then hopped away,
I munched a carrot and cheered: huzzah!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "Feature/claude code bot" refers to the branch name and is too vague to convey the actual scope of changes, which include bot command implementation, event subscription, session management, routing, and extensive documentation. Replace with a more descriptive title that summarizes the primary changes, such as "Add bot subcommand with Claude Code integration" or "Implement Feishu bot with Claude Code message routing".
Description check ❓ Inconclusive The PR description provides substantial technical detail about changes and includes test coverage claims, but the Test Plan section contains unchecked checkboxes and vague references without concrete verification details. Clarify the Test Plan section by either checking the completed items or providing specific verification steps taken. Expand test execution details beyond coverage percentages to include actual test run results and any manual verification performed.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 10, 2026

Greptile Summary

This PR introduces a lark-cli bot command that integrates Claude Code with Feishu/Lark via WebSocket event subscription, allowing users to chat with Claude from Lark messages. Several P1 issues remain after the previous review round that need to be resolved before merging.

  • Post message parsing broken (handler.go): extractTextContent looks for post[\"content\"] but Lark's post format nests content under a locale key (zh_cn/en_us); every post-type message silently delivers empty text to Claude.
  • Claude subprocess runs in wrong directory (claude.go): cmd.Dir is never set and /tmp/lark-claude-bot is never created, so --add-dir references a non-existent path on every invocation.
  • Session files world-readable (session.go): 0755/0644 permissions expose chat IDs and session tokens to all OS users on the host.

Confidence Score: 3/5

Not safe to merge — multiple P1 issues including a compile error, broken post-message parsing, incorrect subprocess working directory, and world-readable session files.

Three confirmed P1 correctness/security bugs remain unresolved: session files are world-readable, the Claude subprocess working directory is never created or set, and Lark post-type message content is silently dropped due to wrong JSON traversal. Additionally subscribe.go references an undefined atomic identifier (missing import + wrong field type), which is a compile error.

shortcuts/bot/handler.go (post parsing), shortcuts/bot/claude.go (workdir), shortcuts/bot/session.go (file permissions), shortcuts/bot/subscribe.go (compile error)

Security Review

  • Session data information disclosure (shortcuts/bot/session.go lines 51, 127): Sessions directory created 0755 and files written 0644, making all session IDs and chat IDs readable by every local OS user.
  • --dangerously-skip-permissions hardcoded (cmd/bot/start.go line 149): SkipPermissions: true is unconditional, granting any Lark user arbitrary file-system and shell access on the host without opt-in.
  • Empty tenant access token on every API call (shortcuts/bot/sender.go lines 63, 88): WithTenantAccessToken(\"\") overrides the SDK's managed auth, causing all message-send calls to fail with auth errors.

Important Files Changed

Filename Overview
shortcuts/bot/claude.go Claude CLI wrapper with retry logic; workdir is never created and cmd.Dir is never set, so every invocation runs in the wrong directory with a non-existent --add-dir path
shortcuts/bot/handler.go Message event parsing and Claude routing; post-type message content extraction always fails due to incorrect assumption about Lark's nested locale→content JSON structure
shortcuts/bot/session.go TTL-based session persistence with atomic writes; directory and files use world-readable permissions (0755/0644) exposing session IDs to all OS users
shortcuts/bot/subscribe.go WebSocket event subscription with graceful shutdown; references undefined atomic identifier (missing import, wrong field type) — compile error
shortcuts/bot/router.go Command and pattern routing with whitelist support; /help handler still acquires a reentrant RLock — potential deadlock under write contention
shortcuts/bot/sender.go Real Lark IM send/reply implementation wired up; passes empty WithTenantAccessToken("") which overrides SDK-managed auth on every API call
cmd/bot/start.go Bot start command; properly creates BotStartOptions from flags, but --config value is captured yet LoadMultiAppConfig() is called without it
cmd/bot/bot.go Top-level bot command wiring; sub-commands now correctly receive their own typed options structs

Sequence Diagram

sequenceDiagram
    participant U as Lark User
    participant LWS as Lark WebSocket
    participant ES as EventSubscriber
    participant BH as BotHandler
    participant CC as ClaudeClient
    participant SM as SessionManager
    participant MS as MessageSender

    U->>LWS: Send message
    LWS->>ES: im.message.receive_v1 event
    ES->>BH: HandleMessage(event)
    BH->>BH: parseMessageEvent()
    BH->>SM: Get(chatID)
    SM-->>BH: sessionData (or nil)
    BH->>CC: ProcessMessage(content, sessionID)
    CC->>CC: exec claude -p ... --output-format json
    CC-->>BH: ClaudeResponse{Result, SessionID}
    BH->>SM: Set(chatID, newSessionID)
    BH-->>ES: response string
    ES->>MS: SendMessage(chatID, response, messageID)
    MS->>LWS: Im.V1.Message.Reply
    LWS-->>U: Reply
Loading

Reviews (9): Last reviewed commit: "Update shortcuts/bot/subscribe.go" | Re-trigger Greptile

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: 14

🧹 Nitpick comments (2)
cmd/bot/start.go (1)

10-10: Consider using vfs.* instead of os.* for filesystem access.

The coding guidelines specify using vfs.* instead of os.* for all filesystem access. While this file currently only uses os.Signal, future implementations involving config file reading should use the virtual filesystem abstraction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/bot/start.go` at line 10, The file currently imports os (used for
os.Signal) but the project guideline requires using the virtual filesystem API
for file operations; keep os for signal handling (os.Signal) but switch any
filesystem calls (e.g., os.Open, os.ReadFile, os.Stat, os.MkdirAll) to use the
vfs package instead (e.g., vfs.Open, vfs.ReadFile, vfs.Stat, vfs.MkdirAll), add
the vfs import, and update any future config/file access code to call the vfs
functions rather than os.* while leaving os.Signal usage as-is.
docs/CODEMAPS/architecture.md (1)

113-113: Tighten wording: use “CLI” instead of “CLI interface.”

This removes redundancy and reads cleaner.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/CODEMAPS/architecture.md` at line 113, Change the redundant phrase "CLI
interface" to "CLI" in the responsibility line so it reads "Responsibility: CLI,
command parsing, user interaction"; locate the exact string "Responsibility: CLI
interface, command parsing, user interaction" in the architecture document and
replace "CLI interface" with "CLI".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/bot/start.go`:
- Around line 80-82: The error returned for daemon mode in cmd/bot/start.go
currently uses errors.New("daemon 模式尚未实现"); update the RunE handler to return
output.Errorf (or output.ErrWithHint if a hint is appropriate) instead, e.g.,
replace the errors.New return in the daemon-mode branch with output.Errorf and
keep the same localized message (or add a short actionable hint via
output.ErrWithHint), ensuring the command's RunE follows the project's
cmd/**/*.go error-return convention.
- Line 57: The startup/shutdown banner and other progress messages are being
written to stdout via fmt.Fprintf(f.IOStreams.Out, ...) but per guidelines
progress/warnings must go to stderr; change those calls (e.g., the banner at
fmt.Fprintf(f.IOStreams.Out, "=== Claude Code Bot 启动中 ===\n") and the similar
prints around lines 74–76) to write to f.IOStreams.ErrOut (or use
fmt.Fprintf(f.IOStreams.ErrOut, ...)) so progress messages go to stderr while
keeping program output on stdout.
- Around line 28-43: Function signature and usage are wrong: newCmdBotStart
currently takes *BotOptions but accesses Ctx, Config, Daemon and calls
botStartRun which expect *BotStartOptions. Change newCmdBotStart to accept
*BotStartOptions (not *BotOptions), update the function parameter name if
needed, keep the flags binding to opts.Config and opts.Daemon and set opts.Ctx =
cmd.Context(), and call botStartRun(opts). Also update any callers that pass a
*BotOptions to instead construct or pass a *BotStartOptions so types align.

In `@cmd/bot/status.go`:
- Around line 22-34: newCmdBotStatus currently accepts *BotStatusOptions but is
called with *BotOptions (from bot.go), causing a type mismatch; update the API
so the types align by either changing newCmdBotStatus's parameter to accept
*BotOptions (and propagate needed fields to botStatusRun) or by constructing a
*BotStatusOptions from the provided *BotOptions before calling newCmdBotStatus;
locate the function newCmdBotStatus and the caller in bot.go and make the
minimal change to use the same option type (BotOptions -> newCmdBotStatus or
convert opts -> BotStatusOptions) and ensure botStatusRun still receives the
context/options it needs.
- Around line 37-56: In botStatusRun, remove or explicitly ignore the unused
variable ctx (currently set as ctx := opts.Ctx) by deleting it or replacing with
_ = opts.Ctx, and send the progress message to the error stream instead of
stdout by changing the fmt.Fprintf target from f.IOStreams.Out to
f.IOStreams.ErrOut so progress/warnings go to stderr while the JSON result
remains printed to f.IOStreams.Out via output.PrintJson; refer to the
botStatusRun function and the opts.Factory f / f.IOStreams.Out and
f.IOStreams.ErrOut symbols to locate where to apply these changes.

In `@cmd/bot/stop.go`:
- Around line 49-58: The command currently writes a progress banner to stdout
and emits a "not_implemented" JSON success envelope then returns nil; change
this so progress/hints go to stderr and the command fails with a structured JSON
error on stdout. Concretely: send the banner using f.IOStreams.Err (not
f.IOStreams.Out), remove the current success envelope, and instead create an
error payload (e.g., {"status":"error","error":"bot stop not
implemented","code":"not_implemented"}) and emit it via
output.PrintJson(f.IOStreams.Out, payload) then return a non-nil error (e.g.,
fmt.Errorf or the existing CLI error type) so the process exits non-zero;
reference f.IOStreams.Out, f.IOStreams.Err and output.PrintJson when making
these changes.
- Around line 40-42: Remove the unused ctx assignment that causes a "declared
but not used" compile error: delete the line "ctx := opts.Ctx" (or replace it
with an underscore if the context must be referenced) so only the used variable
"f := opts.Factory" remains; locate this in the function where opts is used and
adjust any subsequent references to use opts.Ctx directly if needed.

In `@cmd/bot/TEST.md`:
- Around line 47-50: Replace the hardcoded developer path and incorrect build
target in the test instructions: remove the absolute path
(/Users/rongchuanxie/...) and change the build invocation 'go build -o
/tmp/lark-cli ./cmd/root.go' to a portable command such as 'go build -o
/tmp/lark-cli .' (or 'go build -o ./bin/lark-cli .' if you prefer a
repo-relative output), updating the line that contains the exact string 'go
build -o /tmp/lark-cli ./cmd/root.go' in TEST.md.

In `@docs/bot-integration-plan.md`:
- Around line 48-60: The fenced code blocks in docs/bot-integration-plan.md (for
example the block beginning with "lark-cli bot start") lack language identifiers
which trips Markdownlint MD040; update each fence by appending an appropriate
language tag (e.g., ```bash for shell/CLI sequences like the "lark-cli bot
start" block, ```text for plain diagrams, ```go for Go snippets, ```yaml for
YAML snippets) so the blocks at the shown ranges (48-60 and also 64-78, 199-204,
217-219, 232-235, 299-308) and any other fenced blocks in the file all include
the correct language identifier after the opening ``` fence.
- Around line 360-365: The args slice currently unconditionally includes the
dangerous flag "--dangerously-skip-permissions" (see args variable and usage of
content and c.workDir) — make this flag opt-in by gating its inclusion behind an
explicit configuration (e.g., an environment variable like
DANGEROUSLY_SKIP_PERMISSIONS or a boolean field on the caller/config passed into
the method that builds args), so only when that opt-in is true you append
"--dangerously-skip-permissions"; also add a clear inline comment near the args
construction and update any public docs to warn about the security implications
and when it may be safely used.

In `@docs/CODEMAPS/backend.md`:
- Around line 108-113: The docs incorrectly claim RegisterShortcuts
auto-discovers shortcuts from subdirectories; update the documentation to
reflect the actual implementation: describe that RegisterShortcuts consumes the
allShortcuts collection, groups entries by service, and mounts grouped commands
onto the root command (referencing RegisterShortcuts and allShortcuts and the
grouping/mounting behavior) and remove the statement about directory-based
discovery.
- Around line 9-27: The fenced code block that starts with "lark-cli <command>
[subcommand] [flags]" is unlabeled and several tables in
docs/CODEMAPS/backend.md are missing surrounding blank lines, causing
markdownlint errors (MD040, MD058); fix by adding an appropriate language tag
(e.g., ```bash or ```text) to each unlabeled fenced block including the
"lark-cli..." block and the other occurrences referenced (around the sections
you flagged), and ensure every table is preceded and followed by a blank line so
tables are isolated from surrounding text (apply these edits to the other ranges
you listed: 84-98, 172-174, 192-208, 213-245, 305-310).

In `@README.bot.md`:
- Around line 124-138: The code block containing the ASCII architecture diagram
(starting with the line "飞书用户消息" and including "lark-cli event +subscribe
(WebSocket 长连接)") is missing a language specifier; update the opening
triple-backtick fence to include a plain-text specifier (e.g., ```text or
```plaintext) so the block is recognized as plain text in README.bot.md.
- Around line 80-89: The fenced chat example in README.bot.md is missing a
language specifier; update the triple-backtick block that contains the dialogue
lines ("你: 帮我写一个 Python 函数计算斐波那契数列" ... "/run tests") to use a plain-text
specifier (e.g., ```text) so static analysis recognizes it as plain text; ensure
every similar chat/example fenced block in the file uses `text` or `plaintext`
consistently.

---

Nitpick comments:
In `@cmd/bot/start.go`:
- Line 10: The file currently imports os (used for os.Signal) but the project
guideline requires using the virtual filesystem API for file operations; keep os
for signal handling (os.Signal) but switch any filesystem calls (e.g., os.Open,
os.ReadFile, os.Stat, os.MkdirAll) to use the vfs package instead (e.g.,
vfs.Open, vfs.ReadFile, vfs.Stat, vfs.MkdirAll), add the vfs import, and update
any future config/file access code to call the vfs functions rather than os.*
while leaving os.Signal usage as-is.

In `@docs/CODEMAPS/architecture.md`:
- Line 113: Change the redundant phrase "CLI interface" to "CLI" in the
responsibility line so it reads "Responsibility: CLI, command parsing, user
interaction"; locate the exact string "Responsibility: CLI interface, command
parsing, user interaction" in the architecture document and replace "CLI
interface" with "CLI".
🪄 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: d6cc3fb5-a3c6-4e90-85c6-4e7db32233cc

📥 Commits

Reviewing files that changed from the base of the PR and between 474cb30 and 81a5eb5.

📒 Files selected for processing (12)
  • .reports/codemap-diff.txt
  • README.bot.md
  • cmd/bot/TEST.md
  • cmd/bot/bot.go
  • cmd/bot/start.go
  • cmd/bot/status.go
  • cmd/bot/stop.go
  • cmd/root.go
  • docs/CODEMAPS/architecture.md
  • docs/CODEMAPS/backend.md
  • docs/CODEMAPS/dependencies.md
  • docs/bot-integration-plan.md

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 10, 2026

Tip:

Greploop — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.

Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

@github-actions github-actions bot added size/XL Architecture-level or global-impact change and removed size/L Large or sensitive change across domains or core paths labels Apr 10, 2026
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: 12

♻️ Duplicate comments (1)
cmd/bot/start.go (1)

113-115: ⚠️ Potential issue | 🟠 Major

Use output.Errorf instead of errors.New for daemon mode error.

Per coding guidelines, RunE functions must return output.Errorf or output.ErrWithHint. The error message should also be structured and actionable.

Proposed fix
 	// Daemon 模式:后台运行
 	// TODO: 实现 daemon 模式(fork 进程、PID 文件等)
-	return errors.New("daemon 模式尚未实现")
+	return output.Errorf(1, "not_implemented", "daemon mode is not yet implemented; run without --daemon flag")

As per coding guidelines: "RunE functions in commands must return output.Errorf or output.ErrWithHint — never bare fmt.Errorf"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/bot/start.go` around lines 113 - 115, Replace the bare errors.New return
in the daemon branch with the project's output error helper: in the RunE for the
start command (the code returning errors.New("daemon 模式尚未实现")), return
output.ErrWithHint or output.Errorf instead; for example use
output.ErrWithHint("daemon 模式尚未实现", "implement daemon support (forking, PID
file) or run without --daemon") or output.Errorf("daemon 模式尚未实现: %s", "implement
forking and PID file"), so the error follows the RunE guideline and provides a
structured, actionable hint.
🧹 Nitpick comments (4)
shortcuts/bot/handler.go (1)

80-84: Warning output uses fmt.Printf (stdout) instead of stderr.

Per coding guidelines, warnings should go to stderr. However, BotHandler doesn't have access to IOStreams. Consider either:

  1. Adding an optional logger/IOStreams to BotHandlerConfig
  2. Using log.Printf which defaults to stderr
  3. Returning a structured result that includes warnings
Proposed fix using log package
+import "log"
+
 	// Update session
 	if _, err := h.sessionMgr.Set(msgEvent.ChatID, response.SessionID); err != nil {
 		// Log error but don't fail the response
-		fmt.Printf("Warning: failed to save session: %v\n", err)
+		log.Printf("Warning: failed to save session: %v", err)
 	}

As per coding guidelines: "Program output (JSON envelopes) must go to stdout; progress, warnings, and hints must go to stderr"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/bot/handler.go` around lines 80 - 84, The warning currently prints
to stdout via fmt.Printf in the BotHandler session save block; change it to
write to stderr by replacing fmt.Printf with log.Printf (importing the log
package) or, preferably, add an optional logger/IOStreams field to
BotHandlerConfig and BotHandler and use that logger to emit the warning from the
h.sessionMgr.Set error path (reference the h.sessionMgr.Set call and
BotHandler/BotHandlerConfig types so you update the correct place), ensuring
progress/JSON stays on stdout while warnings go to stderr.
shortcuts/bot/router.go (2)

248-259: Consider using sort.Slice instead of manual bubble sort.

While the comment notes pattern count is typically small, using the standard library's sort.Slice is more idiomatic and equally efficient for small collections.

Proposed fix
+import "sort"
+
 // sortPatterns sorts patterns by priority (highest first)
 func (pr *PatternRouter) sortPatterns() {
-	// Simple bubble sort (pattern count is typically small)
-	n := len(pr.patterns)
-	for i := 0; i < n-1; i++ {
-		for j := 0; j < n-i-1; j++ {
-			if pr.patterns[j].Priority < pr.patterns[j+1].Priority {
-				pr.patterns[j], pr.patterns[j+1] = pr.patterns[j+1], pr.patterns[j]
-			}
-		}
-	}
+	sort.Slice(pr.patterns, func(i, j int) bool {
+		return pr.patterns[i].Priority > pr.patterns[j].Priority
+	})
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/bot/router.go` around lines 248 - 259, Replace the manual bubble
sort in PatternRouter.sortPatterns with the standard library sort.Slice to be
more idiomatic: import sort, then call sort.Slice(pr.patterns, func(i, j int)
bool { return pr.patterns[i].Priority > pr.patterns[j].Priority }) so patterns
are sorted by Priority descending; ensure you reference the pr.patterns slice
and the Priority field of the pattern struct in the comparator and remove the
bubble sort loop.

173-176: /clear command is a stub that doesn't actually clear sessions.

The /clear command returns a success message but doesn't interact with SessionManager to delete the session. This needs to be wired up to actually clear the session for the given chatID.

Would you like me to help implement the actual session clearing logic? This would require passing a SessionManager reference to the router or using a callback pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/bot/router.go` around lines 173 - 176, The `/clear` handler
registered via r.RegisterCommand currently only returns a message and must
invoke the session manager to remove the session for chatID; modify the router
to accept or have access to the SessionManager (e.g., add a sessionManager field
on the router or pass it into the factory that sets up RegisterCommand) and
update the handler to call the appropriate method (e.g.,
SessionManager.DeleteSession or SessionManager.ClearSession) with chatID, handle
any returned error and return the success message only on success or an error
otherwise; keep the handler signature (ctx, args, chatID) and ensure you
reference the router's SessionManager inside the closure so the session is
actually cleared.
shortcuts/bot/session.go (1)

36-59: Consider using vfs.* instead of os.* for filesystem access.

Per coding guidelines, use vfs.* instead of os.* for all filesystem access. This file uses os.UserHomeDir, os.MkdirAll, os.Stat, os.ReadFile, os.WriteFile, os.Rename, os.Remove, and os.ReadDir throughout.

If the vfs package provides equivalent functions, consider refactoring to use them for consistency and testability. This would also allow injecting a mock filesystem for unit testing.

As per coding guidelines: "Use vfs.* instead of os.* for all filesystem access"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/bot/session.go` around lines 36 - 59, The code in NewSessionManager
(and related SessionManager file methods) uses os.* helpers (e.g.,
os.UserHomeDir, os.MkdirAll and other os.* calls noted in the review) instead of
the project's vfs abstraction; refactor to use the vfs package equivalents by
injecting or referencing a vfs filesystem into SessionManager (e.g., add a fs
vfs.FS field or accept it via SessionManagerConfig), replace
os.UserHomeDir/os.MkdirAll/etc. calls with vfs methods, and update constructors
and any file operations (Stat/ReadFile/WriteFile/Rename/Remove/ReadDir) across
SessionManager to use that vfs instance for testability and consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/bot/start.go`:
- Around line 57-59: Replace bare fmt.Errorf returns in the RunE handler with
the project output helper: for the ValidateClaudeCLI call (the err check around
bot.ValidateClaudeCLI(ctx)) and the other two error returns referenced (around
lines 68 and 88), change fmt.Errorf("...: %w", err) to output.Errorf("claude CLI
validation failed: %v", err) or output.ErrWithHint(...) if you want to add a
user hint; ensure you keep the original message text but use %v (or appropriate
formatting) instead of %w, and add the output package import if missing so the
RunE function returns output.Errorf/output.ErrWithHint rather than fmt.Errorf.
- Around line 97-98: The call to botHandler.GetStats(ctx) swallows errors and
may cause invalid JSON to be printed; change the code around GetStats to check
the returned error, and if non-nil handle it (e.g., log or print a clear error
to the user and exit with a non-zero status) instead of calling
output.PrintJson(io.Out, stats). Specifically, inspect the error from GetStats,
avoid calling output.PrintJson when err != nil, and use the existing
logging/output mechanism (or io.ErrOut) to surface the error so users aren’t
shown misleading JSON.

In `@shortcuts/bot/claude.go`:
- Around line 58-67: The backoff in the retry loop inside the code that iterates
"for attempt := 0; attempt < c.maxRetries; attempt++" is linear (waitTime :=
time.Duration(attempt) * time.Second) despite the "Exponential backoff" comment;
change waitTime calculation in that loop (the waitTime variable used before the
select) to use an exponential formula such as doubling per attempt (e.g.,
baseDelay * 2^attempt) and optionally clamp to a max delay to avoid unbounded
waits, updating the comment to match; keep the retry loop, ctx cancellation
handling, and time.After usage unchanged.
- Around line 142-148: The retry logic in ProcessMessage currently treats
"context deadline exceeded" as always retryable; change it to only retry when
the per-attempt timeout triggered the deadline, not when the parent context was
cancelled or timed out. Remove or special-case "context deadline exceeded" from
retryablePatterns and, where you handle errors after an attempt (in
ProcessMessage/attempt loop, the per-attempt context creation and error check
around the attempt), check the parent context with parentCtx.Err() (or
errors.Is(err, context.DeadlineExceeded) combined with parentCtx.Err()==nil)
before deciding to retry; if parentCtx.Err() != nil then abort and return the
error, otherwise allow retries for per-attempt deadline exceeded. Ensure you
reference the per-attempt context variable (the one created for each attempt)
and the parent ctx passed into ProcessMessage when implementing this check.
- Around line 12-13: Remove the unused import
"github.com/larksuite/cli/cmd/cmdutil" from shortcuts/bot/claude.go; locate the
import block in claude.go and delete that import entry (ensure any remaining
imports remain properly formatted) so the file no longer imports cmdutil when no
symbols from it are used.

In `@shortcuts/bot/handler.go`:
- Around line 17-19: The CreateTime field in the struct (fields MessageType and
CreateTime) is misaligned; fix the indentation so CreateTime lines up with
MessageType (same leading tabs/spaces) and keep its json tag
`json:"create_time"` unchanged so the struct formatting is consistent and Go
linting/formatting passes (update the struct definition containing MessageType
and CreateTime).
- Around line 183-196: The extractPostText function only flattens a single-level
[]interface{} but Lark posts are nested as {"post": {"zh_cn": {"content":
[[{...}]]}}}; update BotHandler.extractPostText to recursively walk the nested
structure: detect when an item is a map[string]interface{} containing language
keys (e.g., "zh_cn") or "content", handle nested []interface{} paragraphs and
segments, and when a segment is a map with a "text" string append it (preserving
newlines between paragraphs); ensure the function safely handles both the
existing single-level slices and the multi-level post.lang.content arrays by
recursing into slices and maps until all "text" values are collected.

In `@shortcuts/bot/router.go`:
- Around line 160-170: The /help handler deadlocks because Route holds
r.mu.RLock while invoking handlers and the help handler also acquires
r.mu.RLock; fix by changing Route so it only holds the lock to fetch the handler
(and any needed metadata) and then releases the lock before calling the handler:
in Route(), acquire r.mu.RLock(), look up the handler from r.commands into a
local variable, release the lock, then invoke the handler; keep the help handler
(registered in RegisterCommand "help") unchanged so it can safely acquire
r.mu.RLock when building the help text from r.commands.
- Around line 91-139: The Route method currently holds r.mu.RLock() for the
whole call, including during handler execution; change it to only hold the read
lock while you inspect state (resolve aliases, lookup handler and copy
r.defaultHandler), then release the read lock (call r.mu.RUnlock()) before
invoking any handler so handlers can call back into Router; specifically, in
Router.Route gather command, args, resolved target via r.aliases, and the
handler (from r.commands) and/or defaultHandler while under the read lock, then
unlock and call handler(ctx, args, chatID) or defaultHandler(ctx,
[]string{message}, chatID).

In `@shortcuts/bot/session.go`:
- Around line 62-91: Get deadlocks because List holds sm.mu.RLock and calls Get
which tries to RLock again; extract the core read logic into an unlocked helper
(e.g., add func (sm *SessionManager) getUnlocked(chatID string) (*SessionData,
error)) that performs sessionPath resolution, file Stat/Read, json.Unmarshal,
expiration check (calling sm.isExpired and sm.Delete as needed) but does NOT
acquire any locks, then change Get to acquire RLock/RUnlock and call
getUnlocked, and change List to call getUnlocked directly while holding its own
RLock; ensure names referenced: SessionManager, Get, List, getUnlocked,
isExpired, Delete, sessionPath.
- Around line 210-216: sessionPath currently only replaces '/' and '\' and can
still allow traversal via sequences like ".."; update SessionManager.sessionPath
to validate and sanitize the final filename using validate.SafeInputPath before
performing any file I/O: build the candidate filename (e.g., safeChatID +
".json"), call validate.SafeInputPath on that filename (or the joined path with
sm.baseDir as required by the validator), handle validation errors (return a
safe fallback or propagate the error appropriately), and then return
filepath.Join(sm.baseDir, validatedName) so all uses of sessionPath are
guaranteed safe.
- Around line 183-203: CleanupExpired currently holds sm.mu.Lock while calling
sm.List() and sm.Delete(), causing deadlocks because List uses RLock and Delete
uses Lock; fix by adding internal unlocked helpers listUnlocked and
deleteUnlocked that assume the caller holds the appropriate lock, then change
CleanupExpired to: obtain a read lock (or brief write lock) and call
listUnlocked to copy sessions, release the lock, iterate sessions and for each
expired acquire sm.mu.Lock, call deleteUnlocked(session.ChatID) and release the
lock (or batch deletes under one Lock if desired); reference the existing
methods isExpired, List, Delete and add listUnlocked and deleteUnlocked to avoid
calling exported methods while holding locks.

---

Duplicate comments:
In `@cmd/bot/start.go`:
- Around line 113-115: Replace the bare errors.New return in the daemon branch
with the project's output error helper: in the RunE for the start command (the
code returning errors.New("daemon 模式尚未实现")), return output.ErrWithHint or
output.Errorf instead; for example use output.ErrWithHint("daemon 模式尚未实现",
"implement daemon support (forking, PID file) or run without --daemon") or
output.Errorf("daemon 模式尚未实现: %s", "implement forking and PID file"), so the
error follows the RunE guideline and provides a structured, actionable hint.

---

Nitpick comments:
In `@shortcuts/bot/handler.go`:
- Around line 80-84: The warning currently prints to stdout via fmt.Printf in
the BotHandler session save block; change it to write to stderr by replacing
fmt.Printf with log.Printf (importing the log package) or, preferably, add an
optional logger/IOStreams field to BotHandlerConfig and BotHandler and use that
logger to emit the warning from the h.sessionMgr.Set error path (reference the
h.sessionMgr.Set call and BotHandler/BotHandlerConfig types so you update the
correct place), ensuring progress/JSON stays on stdout while warnings go to
stderr.

In `@shortcuts/bot/router.go`:
- Around line 248-259: Replace the manual bubble sort in
PatternRouter.sortPatterns with the standard library sort.Slice to be more
idiomatic: import sort, then call sort.Slice(pr.patterns, func(i, j int) bool {
return pr.patterns[i].Priority > pr.patterns[j].Priority }) so patterns are
sorted by Priority descending; ensure you reference the pr.patterns slice and
the Priority field of the pattern struct in the comparator and remove the bubble
sort loop.
- Around line 173-176: The `/clear` handler registered via r.RegisterCommand
currently only returns a message and must invoke the session manager to remove
the session for chatID; modify the router to accept or have access to the
SessionManager (e.g., add a sessionManager field on the router or pass it into
the factory that sets up RegisterCommand) and update the handler to call the
appropriate method (e.g., SessionManager.DeleteSession or
SessionManager.ClearSession) with chatID, handle any returned error and return
the success message only on success or an error otherwise; keep the handler
signature (ctx, args, chatID) and ensure you reference the router's
SessionManager inside the closure so the session is actually cleared.

In `@shortcuts/bot/session.go`:
- Around line 36-59: The code in NewSessionManager (and related SessionManager
file methods) uses os.* helpers (e.g., os.UserHomeDir, os.MkdirAll and other
os.* calls noted in the review) instead of the project's vfs abstraction;
refactor to use the vfs package equivalents by injecting or referencing a vfs
filesystem into SessionManager (e.g., add a fs vfs.FS field or accept it via
SessionManagerConfig), replace os.UserHomeDir/os.MkdirAll/etc. calls with vfs
methods, and update constructors and any file operations
(Stat/ReadFile/WriteFile/Rename/Remove/ReadDir) across SessionManager to use
that vfs instance for testability and consistency.
🪄 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: 856ed970-9f4a-4d16-baa3-2fe852afbe30

📥 Commits

Reviewing files that changed from the base of the PR and between 6019768 and 24155ae.

📒 Files selected for processing (5)
  • cmd/bot/start.go
  • shortcuts/bot/claude.go
  • shortcuts/bot/handler.go
  • shortcuts/bot/router.go
  • shortcuts/bot/session.go

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: 5

♻️ Duplicate comments (2)
cmd/bot/start.go (2)

64-66: ⚠️ Potential issue | 🟠 Major

Use output.Errorf instead of fmt.Errorf for RunE errors.

Per coding guidelines for cmd/**/*.go, RunE functions must return output.Errorf or output.ErrWithHint—never bare fmt.Errorf. This applies to lines 65, 75, 95, 108, and 129.

🐛 Suggested fix for line 65
 	if err := bot.ValidateClaudeCLI(ctx); err != nil {
-		return fmt.Errorf("claude CLI validation failed: %w", err)
+		return output.ErrWithHint(output.ExitRuntime, "claude_cli", fmt.Sprintf("validation failed: %v", err), "ensure 'claude' CLI is installed and in PATH")
 	}

Apply similar changes to the other fmt.Errorf returns at lines 75, 95, 108, and 129, using appropriate error codes and actionable hints.

As per coding guidelines: "RunE functions in commands must return output.Errorf or output.ErrWithHint — never bare fmt.Errorf"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/bot/start.go` around lines 64 - 66, Replace bare fmt.Errorf returns in
the RunE for the bot start command with output.Errorf or output.ErrWithHint:
where you currently return fmt.Errorf("claude CLI validation failed: %w", err)
after calling bot.ValidateClaudeCLI(ctx), change to output.Errorf with an
appropriate error code and include err (e.g.,
output.Errorf(output.CodeInvalidInput, "claude CLI validation failed: %v", err))
or output.ErrWithHint when a user-actionable hint is needed; apply the same
pattern to the other fmt.Errorf sites in this function (the returns after the
calls/conditions around starting the bot, initializing clients, or config
validation) so every RunE return uses output.Errorf/output.ErrWithHint, includes
the wrapped error message, and supplies actionable hints where appropriate.

60-68: ⚠️ Potential issue | 🟡 Minor

Progress messages should go to stderr, not stdout.

Per coding guidelines, program output (JSON envelopes) goes to stdout while progress, warnings, and hints go to stderr. All the startup progress messages should use io.ErrOut.

♻️ Suggested fix pattern
-	fmt.Fprintf(io.Out, "=== Claude Code Bot 启动中 ===\n")
+	fmt.Fprintf(io.ErrOut, "=== Claude Code Bot 启动中 ===\n")

-	fmt.Fprintf(io.Out, "验证 Claude Code CLI...\n")
+	fmt.Fprintf(io.ErrOut, "验证 Claude Code CLI...\n")

Apply the same change to all progress messages (lines 60, 63, 67, 70, 77, 88, 97, 100, 123, 126, 132).

As per coding guidelines: "Program output (JSON envelopes) must go to stdout; progress, warnings, and hints must go to stderr"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/bot/start.go` around lines 60 - 68, The progress prints in the start
routine are currently writing to stdout via fmt.Fprintf(io.Out, ...) but per
guidelines must go to stderr; update all progress/warning/hint fmt.Fprintf calls
in the start function (e.g., the calls printing "=== Claude Code Bot 启动中 ===",
"验证 Claude Code CLI...", "✓ Claude Code CLI 已就绪" and the other startup messages)
to use io.ErrOut instead of io.Out so program output stays on stdout and
progress goes to stderr.
🧹 Nitpick comments (6)
shortcuts/bot/sender.go (2)

31-43: Unused helper function buildMessageContent.

This function is defined but never called. Consider removing it until the actual Lark API integration is implemented, or add a TODO comment explaining its intended use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/bot/sender.go` around lines 31 - 43, The helper function
buildMessageContent on type MessageSender is currently unused; either remove the
entire buildMessageContent method to eliminate dead code or keep it but add a
clear TODO comment above the function explaining its intended purpose (building
Lark message JSON) and when it will be used, so future Lark API integration
won’t treat it as accidental dead code; update references to MessageSender only
if you remove the function.

19-29: Progress/debug output should go to stderr, not stdout.

The [TODO] placeholder message is progress/diagnostic output. Per coding guidelines, it should be written to stderr rather than stdout (which is reserved for JSON envelopes/program output).

♻️ Suggested fix
-	fmt.Printf("[TODO] Send message to chat %s: %s\n", chatID, message)
+	fmt.Fprintf(os.Stderr, "[TODO] Send message to chat %s: %s\n", chatID, message)

Add "os" to the imports.

Based on learnings: "Program output (JSON envelopes) must go to stdout; progress, warnings, and hints must go to stderr"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/bot/sender.go` around lines 19 - 29, The placeholder diagnostic
print in MessageSender.SendMessage writes progress to stdout; change it to write
to stderr by importing "os" and replacing fmt.Printf with
fmt.Fprintln(os.Stderr, ...) (or equivalent) inside the SendMessage method so
the "[TODO] Send message to chat ..." diagnostic goes to stderr while keeping
the rest of the function unchanged.
docs/CODEMAPS/backend.md (1)

300-313: Error handling pattern example uses fmt.Errorf but cmd/**/*.go requires output.Errorf.

The documented error handling pattern shows fmt.Errorf, but per coding guidelines, cmd/**/*.go files must use output.Errorf or output.ErrWithHint. Consider adding a note that command handlers have different requirements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/CODEMAPS/backend.md` around lines 300 - 313, The docs example shows
using fmt.Errorf for wrapping errors but command files under cmd/**/*.go must
use output.Errorf or output.ErrWithHint; update the backend.md Error Handling
Pattern to note this exception and either add an alternative example
demonstrating output.Errorf/output.ErrWithHint for command handlers or a short
note stating that fmt.Errorf is acceptable elsewhere but command handlers should
use output.Errorf/output.ErrWithHint (reference the fmt.Errorf, output.Errorf,
output.ErrWithHint symbols and the cmd/**/*.go/command handler context).
docs/CODEMAPS/architecture.md (1)

17-76: Add language specifier to fenced code blocks.

The ASCII diagram blocks should have a language tag (e.g., text or plaintext) to satisfy markdownlint MD040 and improve rendering consistency.

♻️ Example fix for the first block
-```
+```text
 ┌─────────────────────────────────────────────────────────────┐
 │                        User/AI Agent                        │

Apply similar changes to the other fenced blocks at lines 147, 169, and 209.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/CODEMAPS/architecture.md` around lines 17 - 76, The fenced ASCII diagram
blocks in docs/CODEMAPS/architecture.md (e.g., the block starting with the
"User/AI Agent" diagram and the other ASCII diagram blocks in the file) are
missing a language specifier; update each triple-backtick fence to include a
plain-text language tag such as ```text or ```plaintext to satisfy markdownlint
MD040 and ensure consistent rendering across the diagrams.
cmd/bot/start.go (1)

48-49: --config and --daemon flags are defined but unused.

The --config flag is accepted but opts.Config is never used in botStartRun. The --daemon flag is similarly unused. Consider either implementing these features or removing the flags to avoid user confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/bot/start.go` around lines 48 - 49, The flags registered with
cmd.Flags().StringVar(&config, "config", ...) and cmd.Flags().BoolVar(&daemon,
"daemon", ...) are unused in botStartRun (opts.Config is never read and daemon
is ignored); either remove these flag registrations or wire them into startup
logic: read the parsed config string into opts.Config (or pass it to the
function that loads configuration) and handle the daemon bool (e.g., set
opts.Daemon, call the daemonize routine, or early-return with background spawn);
update references to config/daemon or opts.Config/opts.Daemon so the variables
are actually consumed by botStartRun and any config-loading/daemonizing helper
functions.
shortcuts/bot/subscribe.go (1)

155-177: Event body is parsed twice - once in handler, once in sendReply.

The event.Body JSON is parsed in createEventHandler (line 110) and again in sendReply (line 159). Consider extracting chat_id and message_id during the first parse and passing them through the call chain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/bot/subscribe.go` around lines 155 - 177, The sendReply function
currently reparses event.Body to extract chat_id and message_id; instead,
extract chat_id and message_id once in createEventHandler when event.Body is
first unmarshaled and pass those values down to sendReply (or attach them to
EventSubscriber/event context) so sendReply no longer calls json.Unmarshal on
event.Body. Update createEventHandler to set chatID and messageID variables (or
populate a small struct/fields on EventSubscriber) and change sendReply
signature to accept chatID and messageID (or read from those fields) and then
call s.sender.SendMessage(ctx, chatID, message, messageID), removing the
duplicate parsing logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/bot/start.go`:
- Around line 79-85: The WorkDir for claudeClient is hardcoded to "/tmp/..."
which breaks on Windows; change the WorkDir in the bot.NewClaudeClient call to
use a cross-platform temp path (e.g., filepath.Join(os.TempDir(),
"lark-claude-bot")) and update imports to include "os" and "path/filepath";
adjust the claudeClient initialization where bot.ClaudeClientConfig{ WorkDir:
... } is set so it builds the path at runtime instead of the hardcoded string.

In `@docs/CODEMAPS/backend.md`:
- Around line 192-206: The table row for the "Sender" component has a malformed
cell: the file path token `shortcuts/bot/sender.go is missing its closing
backtick; open docs/CODEMAPS/backend.md, locate the "Sender" row (the line
containing Sender and the file path), add the missing closing backtick so the
cell reads `shortcuts/bot/sender.go`, and confirm the table's pipe separators
remain correctly aligned.

In `@shortcuts/bot/subscribe.go`:
- Around line 188-193: The info method on EventSubscriber currently writes to
stdout via fmt.Println; change it to write to stderr (e.g., use
fmt.Fprintln(os.Stderr, msg)) so progress/info messages don't pollute program
stdout JSON envelopes, and add the os import if missing; keep the quiet check
and the EventSubscriber.info signature unchanged.
- Around line 79-84: The "Connected" message is printed before cli.Start(ctx)
actually succeeds; modify the logic so the success message is only shown after
startup completes: after launching the goroutine that sends the result to
startErrCh (where cli.Start(ctx) is invoked), read from startErrCh, check the
returned error, and call s.info("Connected. Waiting for events... (Ctrl+C to
stop)") only if the error is nil; otherwise log the error and exit/handle it.
Use the existing startErrCh, cli.Start(ctx), and s.info symbols to locate and
update the code.
- Around line 25-26: Replace the plain int eventCount field with sync/atomic's
atomic.Int64 (e.g., eventCount atomic.Int64) and update all accesses: in the
event handler increment using s.eventCount.Add(1), in the signal handler read
with s.eventCount.Load(), and in GetStats() return the loaded value
(s.eventCount.Load()) where "events_received" is produced; also add the
sync/atomic import and convert the loaded int64 as needed for any JSON or
formatting consumers.

---

Duplicate comments:
In `@cmd/bot/start.go`:
- Around line 64-66: Replace bare fmt.Errorf returns in the RunE for the bot
start command with output.Errorf or output.ErrWithHint: where you currently
return fmt.Errorf("claude CLI validation failed: %w", err) after calling
bot.ValidateClaudeCLI(ctx), change to output.Errorf with an appropriate error
code and include err (e.g., output.Errorf(output.CodeInvalidInput, "claude CLI
validation failed: %v", err)) or output.ErrWithHint when a user-actionable hint
is needed; apply the same pattern to the other fmt.Errorf sites in this function
(the returns after the calls/conditions around starting the bot, initializing
clients, or config validation) so every RunE return uses
output.Errorf/output.ErrWithHint, includes the wrapped error message, and
supplies actionable hints where appropriate.
- Around line 60-68: The progress prints in the start routine are currently
writing to stdout via fmt.Fprintf(io.Out, ...) but per guidelines must go to
stderr; update all progress/warning/hint fmt.Fprintf calls in the start function
(e.g., the calls printing "=== Claude Code Bot 启动中 ===", "验证 Claude Code
CLI...", "✓ Claude Code CLI 已就绪" and the other startup messages) to use
io.ErrOut instead of io.Out so program output stays on stdout and progress goes
to stderr.

---

Nitpick comments:
In `@cmd/bot/start.go`:
- Around line 48-49: The flags registered with cmd.Flags().StringVar(&config,
"config", ...) and cmd.Flags().BoolVar(&daemon, "daemon", ...) are unused in
botStartRun (opts.Config is never read and daemon is ignored); either remove
these flag registrations or wire them into startup logic: read the parsed config
string into opts.Config (or pass it to the function that loads configuration)
and handle the daemon bool (e.g., set opts.Daemon, call the daemonize routine,
or early-return with background spawn); update references to config/daemon or
opts.Config/opts.Daemon so the variables are actually consumed by botStartRun
and any config-loading/daemonizing helper functions.

In `@docs/CODEMAPS/architecture.md`:
- Around line 17-76: The fenced ASCII diagram blocks in
docs/CODEMAPS/architecture.md (e.g., the block starting with the "User/AI Agent"
diagram and the other ASCII diagram blocks in the file) are missing a language
specifier; update each triple-backtick fence to include a plain-text language
tag such as ```text or ```plaintext to satisfy markdownlint MD040 and ensure
consistent rendering across the diagrams.

In `@docs/CODEMAPS/backend.md`:
- Around line 300-313: The docs example shows using fmt.Errorf for wrapping
errors but command files under cmd/**/*.go must use output.Errorf or
output.ErrWithHint; update the backend.md Error Handling Pattern to note this
exception and either add an alternative example demonstrating
output.Errorf/output.ErrWithHint for command handlers or a short note stating
that fmt.Errorf is acceptable elsewhere but command handlers should use
output.Errorf/output.ErrWithHint (reference the fmt.Errorf, output.Errorf,
output.ErrWithHint symbols and the cmd/**/*.go/command handler context).

In `@shortcuts/bot/sender.go`:
- Around line 31-43: The helper function buildMessageContent on type
MessageSender is currently unused; either remove the entire buildMessageContent
method to eliminate dead code or keep it but add a clear TODO comment above the
function explaining its intended purpose (building Lark message JSON) and when
it will be used, so future Lark API integration won’t treat it as accidental
dead code; update references to MessageSender only if you remove the function.
- Around line 19-29: The placeholder diagnostic print in
MessageSender.SendMessage writes progress to stdout; change it to write to
stderr by importing "os" and replacing fmt.Printf with fmt.Fprintln(os.Stderr,
...) (or equivalent) inside the SendMessage method so the "[TODO] Send message
to chat ..." diagnostic goes to stderr while keeping the rest of the function
unchanged.

In `@shortcuts/bot/subscribe.go`:
- Around line 155-177: The sendReply function currently reparses event.Body to
extract chat_id and message_id; instead, extract chat_id and message_id once in
createEventHandler when event.Body is first unmarshaled and pass those values
down to sendReply (or attach them to EventSubscriber/event context) so sendReply
no longer calls json.Unmarshal on event.Body. Update createEventHandler to set
chatID and messageID variables (or populate a small struct/fields on
EventSubscriber) and change sendReply signature to accept chatID and messageID
(or read from those fields) and then call s.sender.SendMessage(ctx, chatID,
message, messageID), removing the duplicate parsing logic.
🪄 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: 2f730e6a-2332-47e5-90d2-6f436315f041

📥 Commits

Reviewing files that changed from the base of the PR and between 24155ae and bb71472.

📒 Files selected for processing (10)
  • .gitignore
  • .reports/codemap-diff.txt
  • cmd/bot/start.go
  • cmd/bot/status.go
  • cmd/bot/stop.go
  • docs/CODEMAPS/architecture.md
  • docs/CODEMAPS/backend.md
  • shortcuts/bot/claude.go
  • shortcuts/bot/sender.go
  • shortcuts/bot/subscribe.go
💤 Files with no reviewable changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (3)
  • cmd/bot/status.go
  • cmd/bot/stop.go
  • shortcuts/bot/claude.go

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: 2

🧹 Nitpick comments (1)
shortcuts/bot/handler_test.go (1)

73-77: Avoid relying on nil-receiver behavior in these tests.

NewBotHandler rejects a nil SessionManager, so both setups return err and a nil handler. The subtests only work because extractTextContent / parseMessageEvent currently avoid touching receiver state. Please construct a valid handler or instantiate &BotHandler{} directly for these helper-level tests so they stay aligned with the constructor contract.

Also applies to: 188-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/bot/handler_test.go` around lines 73 - 77, Tests currently pass a
nil SessionManager to NewBotHandler which violates its contract and results in
err + nil handler; update the tests to either construct a valid handler via
NewBotHandler with a real SessionManager mock/fixture or bypass the constructor
by instantiating &BotHandler{} directly for helper-level tests that only
exercise extractTextContent and parseMessageEvent; change both occurrences
(around the shown block and the similar setup at lines ~188-192) to use a proper
SessionManager mock or &BotHandler{} so the tests no longer rely on nil-receiver
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/bot/claude_test.go`:
- Around line 103-152: The test currently calls the test-only
ClaudeResponse.UnmarshalJSON stub (which always returns nil and doesn't populate
fields), so replace that call in TestClaudeResponse with the real JSON
unmarshal: use json.Unmarshal([]byte(tt.json), &resp) and ensure the test
imports "encoding/json"; this will exercise actual parsing for the
ClaudeResponse struct instead of the stubbed UnmarshalJSON method.

In `@shortcuts/bot/session_test.go`:
- Around line 171-203: The test creates both sessions before the sleep so both
exceed the 50ms TTL and will be expired; update the test to ensure "chat_active"
remains fresh by either (a) creating or updating "chat_active" after the 60ms
sleep (use SessionManager.Set("chat_active", ...) again) or (b) use a larger TTL
in the SessionManagerConfig for the active session test; ensure the code paths
referencing NewSessionManager, SessionManagerConfig (TTL), Set, CleanupExpired,
and Get reflect this change so CleanupExpired returns count == 1 and
Get("chat_active") is non-nil.

---

Nitpick comments:
In `@shortcuts/bot/handler_test.go`:
- Around line 73-77: Tests currently pass a nil SessionManager to NewBotHandler
which violates its contract and results in err + nil handler; update the tests
to either construct a valid handler via NewBotHandler with a real SessionManager
mock/fixture or bypass the constructor by instantiating &BotHandler{} directly
for helper-level tests that only exercise extractTextContent and
parseMessageEvent; change both occurrences (around the shown block and the
similar setup at lines ~188-192) to use a proper SessionManager mock or
&BotHandler{} so the tests no longer rely on nil-receiver behavior.
🪄 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: d8f8ba26-7b16-405f-932f-aa8cff145e0b

📥 Commits

Reviewing files that changed from the base of the PR and between bb71472 and a5bff58.

📒 Files selected for processing (4)
  • shortcuts/bot/claude_test.go
  • shortcuts/bot/handler_test.go
  • shortcuts/bot/router_test.go
  • shortcuts/bot/session_test.go
✅ Files skipped from review due to trivial changes (1)
  • shortcuts/bot/router_test.go

richardiitse and others added 16 commits April 11, 2026 02:11
新增文档:
- README.bot.md: Bot 功能说明和使用指南
- docs/bot-integration-plan.md: 完整的技术方案设计

主要内容包括:
- 项目目标:飞书 Bot + Claude Code 集成
- 核心功能:会话管理、命令路由、文件操作
- 架构设计:复用 lark-cli 现有基础设施
- 开发计划:4 个 Phase,预计 4-6 天
新增功能:
- cmd/bot/: 创建 Bot 子命令框架
  - bot.go: Bot 命令入口,包含 start/status/stop 子命令
  - start.go: 启动 Bot(基础框架,TODO: 完整实现)
  - status.go: 查看 Bot 状态(基础框架,TODO: 实现)
  - stop.go: 停止 Bot(基础框架,TODO: 实现)
- cmd/root.go: 注册 bot 命令到命令树

实现进度:
- ✅ 命令框架结构
- ✅ cobra 命令注册
- ⏳ 实际功能实现(Phase 1 进行中)

下一步:
- 集成 event +subscribe
- 实现 session 管理
- 实现 Claude Code 集成
- 实现命令路由

相关文档:README.bot.md, docs/bot-integration-plan.md
添加测试验证文档:
- 静态代码验证清单(全部通过)
- 动态功能测试指南(需要 Go 环境)
- 编译和功能测试步骤
- 当前实现状态说明

静态验证结果:
- ✅ 代码结构正确
- ✅ 导入和注册正确
- ✅ 命令模式一致
- ✅ 版权和注释完整

下一步:安装 Go 后进行编译测试
生成代码地图(Code Maps):
- docs/CODEMAPS/architecture.md - 整体架构设计
- docs/CODEMAPS/backend.md - 后端实现详解
- docs/CODEMAPS/dependencies.md - 依赖项清单
- .reports/codemap-diff.txt - 生成报告

项目分析:
- 项目类型:Go CLI 工具(飞书/Lark)
- 代码规模:520 个 Go 文件
- 命令数量:10 个内置命令 + 200+ shortcuts
- 业务领域:12 个(日历、消息、文档、表格等)
- AI Skills:20 个 Agent Skills

架构亮点:
- 三层命令系统(Shortcuts → API Commands → Raw API)
- 事件订阅(WebSocket 长连接)
- AI Agent 友好设计
- 新增:Bot 功能框架(feature/claude-code-bot)

Token优化:
- 使用 ASCII 图表
- 函数签名替代完整实现
- 结构化列表便于扫描
- 每个文档 < 1000 tokens

生成时间:2026-04-10
文件扫描:520 个 Go 文件
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Implement 4 core modules for Phase 2 of bot development:

- claude.go: Claude Code CLI integration with retry logic
- session.go: Session persistence with TTL and cleanup
- handler.go: Message event processing and routing
- router.go: Command routing with whitelist and pattern matching

Updated start.go to initialize all modules and validate Claude CLI.

Status: Core modules complete, event subscription integration pending.
Implement event subscription integration for Phase 1:

- subscribe.go: WebSocket event subscriber with graceful shutdown
  - Connects to Lark event stream via WebSocket
  - Handles im.message.receive_v1 events
  - Routes events to bot handler for processing

- sender.go: Message sender for replying to Lark
  - Placeholder for im +messages-send integration
  - Builds message content JSON
  - Creates Lark API request objects

- start.go: Updated to initialize and start event subscriber
  - Removed placeholder code
  - Now blocks on event subscription
  - Graceful shutdown on Ctrl+C

Status: Event subscription complete, reply sending needs im integration.
Updated 3 codemap documents to reflect Bot integration status:

- architecture.md: Added bot module to system diagram, updated file counts
- backend.md: Detailed Bot implementation with complete data flow
- codemap-diff.txt: Full analysis report showing 549 files (+29)

Key updates:
- Bot integration: ~1,200 lines across 6 core modules
- Architecture: Complete Feishu → Claude → Feishu flow
- Progress: ~80% complete, reply sending pending

Also removed docs/ from .gitignore to track documentation.
Fixed multiple compilation issues:

1. Import path corrections:
   - cmd/bot/start.go: cmd/cmdutil → internal/cmdutil
   - shortcuts/bot/claude.go: removed unused cmdutil import
   - shortcuts/bot/subscribe.go: added dispatcher import, fixed larkws alias

2. Type fixes:
   - cmd/bot/start.go: use core.LoadMultiAppConfig() instead of non-existent InitializedRuntime
   - shortcuts/bot/subscribe.go: SecretInput type support (use .Plain field)
   - shortcuts/bot/sender.go: commented out TODO im.CreateMessageReq usage

3. Unused variable cleanup:
   - cmd/bot/status.go: removed unused ctx variable
   - cmd/bot/stop.go: removed unused ctx variable

4. Command flow:
   - cmd/bot/start.go: create BotStartOptions in RunE, not modify BotOptions
   - cmd/bot/status.go, cmd/bot/stop.go: removed opts.Ctx assignments

Result: lark-cli now compiles successfully with Go 1.26.2.
Verified: ./lark-cli bot --help works correctly.
Added 4 test files for bot core modules:

1. session_test.go (355 lines)
   - Session manager creation and initialization
   - Get/Set/Delete operations
   - TTL expiration and cleanup
   - Special characters handling (Chinese, slashes)
   - Concurrent access (thread safety)
   - List operations

2. router_test.go (280 lines)
   - Router creation and built-in commands
   - Custom command registration
   - Command aliases
   - Whitelist enforcement
   - Pattern-based routing with priority
   - Fallback handlers
   - Command argument parsing

3. claude_test.go (180 lines)
   - Claude client creation and configuration
   - Default value handling
   - Retry logic for transient errors
   - Context cancellation handling
   - JSON response parsing

4. handler_test.go (290 lines)
   - Bot handler creation and validation
   - Message event parsing
   - Text content extraction (text/post types)
   - Error handling for malformed events
   - Statistics retrieval

Test Coverage Goals:
- Happy path: Core functionality with valid inputs
- Error handling: Invalid inputs, missing data, failures
- Edge cases: Empty values, special characters, concurrency
- Thread safety: Concurrent get/set operations

Status: Test framework established, ready for execution.
- 修复 Get() 持有读锁时调用 Delete(需要写锁) 导致的死锁
- 修复 CleanupExpired() 持有写锁时调用 List(需要读锁) 导致的死锁
- 修复 handler_test.go 中 missing header → missing event data
- 移除递归 UnmarshalJSON 导致的栈溢出
- 修复 router_test.go 参数解析测试(使用自定义命令)
- 添加 sender_test.go 消息发送模块测试
- 添加 subscribe_test.go 事件订阅辅助方法测试
- 覆盖率: 58.2% → 72.1%
- 添加 subscribe_integration_test.go 集成测试
  - handleMessageEvent 成功路径测试
  - sendReply 错误路径测试
  - createEventHandler 边界情况测试
- 修复 sendReply 和 createEventHandler 的 nil 检查
- 使 MessageSender 可注入,便于测试
- 覆盖率: 76.6% → 85.1%
- Update scan date from 2026-04-10 to 2026-04-11
- Update file count: 549→558 (architecture), 520→558 (backend)
- Mark bot implementation as Complete with 85.1% test coverage
- Ignore docs/ coverage artifacts in .gitignore
- Replace placeholder sender with real lark-sdk-go client
- Add LarkClient to EventSubscriber via app credentials
- Implement SendMessage via im.V1.Message.Create API
- Implement sendReply via im.V1.Message.Reply API
- Add NewMessageSenderWithClient for real client injection
- Keep NewMessageSender() for nil-client test compatibility
- Update tests to reflect new API contract (nil client = error)
@richardiitse richardiitse force-pushed the feature/claude-code-bot branch from a5bff58 to 6774e3a Compare April 10, 2026 18:12
@richardiitse
Copy link
Copy Markdown
Author

add lark cli bot to connect Claude Code

- Use &BotHandler{} for helper tests that don't need SessionManager
- Add clarifying comment for CleanupExpired test timing
Comment on lines +75 to +86
response, err := h.claudeClient.ProcessMessage(ctx, msgEvent.Content, sessionID)
if err != nil {
return "", fmt.Errorf("failed to process message with claude: %w", err)
}

// Update session
if _, err := h.sessionMgr.Set(msgEvent.ChatID, response.SessionID); err != nil {
// Log error but don't fail the response
fmt.Printf("Warning: failed to save session: %v\n", err)
}

return response.Result, 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.

P1 Router is never wired into message handling

BotHandler.HandleMessage sends every incoming message directly to Claude via claudeClient.ProcessMessage, completely bypassing the Router defined in router.go. As a result, the built-in commands /help, /status, and /clear registered in registerBuiltInCommands() are unreachable — a user sending /clear will have their message forwarded verbatim to Claude instead of clearing the session.

The handler needs to consult the router first; something like:

// Process message with Router (handles commands like /help, /clear)
response, err := h.router.Route(ctx, msgEvent.Content, msgEvent.ChatID)
if err != nil {
    // fall through to Claude
    claudeResp, claudeErr := h.claudeClient.ProcessMessage(ctx, msgEvent.Content, sessionID)
    ...
}

BotHandler would need a router *Router field wired in from the constructor.

Comment on lines +103 to +109
config, err := core.LoadMultiAppConfig()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return output.ErrWithHint(output.ExitValidation, "config", "not configured", "run: lark-cli config init")
}
return fmt.Errorf("failed to load config: %w", 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.

P1 --config flag is silently ignored

opts.Config holds the value of the --config CLI flag, but botStartRun calls core.LoadMultiAppConfig() unconditionally on line 103, never consulting opts.Config. Any path the user passes with --config is parsed and discarded — the flag is a no-op.

If core.LoadMultiAppConfig has an overload or the caller is expected to pass a path, use opts.Config here:

config, err := core.LoadMultiAppConfig(/* opts.Config */)

Otherwise, remove the flag and its corresponding BotStartOptions.Config field to avoid misleading users.

richardiitse and others added 2 commits April 12, 2026 19:42
- Rewrite README.bot.md to remove fork-specific references
- Update cmd/bot/TEST.md with actual implementation status
- Fix docs/bot-integration-plan.md directory structure references
- Mark all Phase items as completed
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Comment on lines +84 to +112
func (c *ClaudeClient) processMessageOnce(ctx context.Context, message string, sessionID string) (*ClaudeResponse, error) {
// Build command arguments
args := []string{
"-p", message,
"--output-format", "json",
"--add-dir", c.workDir,
}

if c.skipPermissions {
args = append(args, "--dangerously-skip-permissions")
}

if sessionID != "" {
args = append(args, "--resume", sessionID)
}

// Create command with timeout
cmdCtx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()

cmd := exec.CommandContext(cmdCtx, "claude", args...)

// Capture stdout and stderr
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

// Execute command
err := cmd.Run()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Working directory never created and never set on subprocess

c.workDir (hardcoded to /tmp/lark-claude-bot in botStartRun) is never created anywhere before processMessageOnce uses it. This means --add-dir c.workDir passes a non-existent path to the claude CLI on every invocation. Additionally, cmd.Dir is never set, so the claude subprocess runs in whatever directory the lark-cli process was launched from rather than the intended workdir.

Both issues together mean that every Claude invocation either errors on --add-dir validation or operates in the wrong directory, breaking file-context features.

func (c *ClaudeClient) processMessageOnce(ctx context.Context, message string, sessionID string) (*ClaudeResponse, error) {
    // Ensure working directory exists
    if c.workDir != "" {
        if err := os.MkdirAll(c.workDir, 0750); err != nil {
            return nil, fmt.Errorf("failed to create work dir: %w", err)
        }
    }
    // ... build args ...
    cmd := exec.CommandContext(cmdCtx, "claude", args...)
    cmd.Dir = c.workDir   // ← add this
    // ...
}

}

// Ensure base directory exists
if err := os.MkdirAll(config.BaseDir, 0755); err != 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.

P1 security Session files world-readable — information disclosure

The sessions directory is created with 0755 and each session file is written with 0644 (line 127). On any multi-user system this lets every local OS user enumerate all active bot sessions, read chat IDs, and capture the Claude session IDs (which can be used to resume conversations). The directory and files should be owner-only:

Suggested change
if err := os.MkdirAll(config.BaseDir, 0755); err != nil {
if err := os.MkdirAll(config.BaseDir, 0700); err != nil {

And on line 127:

if err := os.WriteFile(tmpPath, data, 0600); err != nil {

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

Labels

size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants