Skip to content

feat: 支持 agent 发送图片给用户 (feishu_send_image)#261

Merged
lishuceo merged 4 commits into
mainfrom
feat/feishu-send-image
Jun 14, 2026
Merged

feat: 支持 agent 发送图片给用户 (feishu_send_image)#261
lishuceo merged 4 commits into
mainfrom
feat/feishu-send-image

Conversation

@lishuceo

Copy link
Copy Markdown
Owner

背景

此前飞书集成只能用户发来的图(feishu_download_message_image),不能图给用户——client.ts 的发送方法只有 text/post/interactive,没有图片上传/发送能力。

本 PR 实现方案 A(独立 msg_type:'image' 消息),让 agent 把工作区里生成的图片(截图、图表、渲染结果)发回当前会话。

改动

src/feishu/client.ts — 三个新方法:

  • uploadImage(buf|stream) → 调 im.image.createimage_key(该接口返回已解包{ image_key },与其它 message 接口的 {code,msg,data} 不同)
  • sendImage(chatId, imageKey)msg_type:'image' 发到会话
  • replyImageInThread(messageId, imageKey)reply_in_thread 回复到话题

src/feishu/tools/send-image.ts(新)— feishu_send_image MCP 工具:

  • 入参 file_path(本地绝对路径)
  • 校验:存在性、isFile、大小 ≤ 10MB、非空、magic bytes 为图片(png/jpg/gif/webp/bmp)
  • 上传后按 threadReplyMsgId 路由:话题内回复到话题,否则发到会话

接线tools/index.ts 增加 threadReplyMsgId 参数并 gate on chatId 注册;executor.ts 透传 ExecuteInput.threadRootMessageId

测试

  • send-image.test.ts:11 个用例(成功会话/话题、文件不存在/过大/为空、非图片、上传失败、发送失败、异常、jpg 识别)
  • index.test.ts:更新工具计数(chatId 存在=12,缺省不注册 send-image)
  • npm run typecheck 通过;npx vitest run 全量 1696 通过(唯一失败的 memory/quality.test.ts 为数据依赖的检索质量用例,基线同样失败,与本 PR 无关)

范围说明

方案 B(在 post/card 里图文混排)作为后续增强,本 PR 不含。

🤖 Generated with Claude Code

lishuceo and others added 4 commits June 14, 2026 21:35
新增 FeishuClient.uploadImage / sendImage / replyImageInThread:
- uploadImage 调 im.image.create 上传图片拿 image_key
  (注意该接口直接返回已解包的 { image_key },非 { code,msg,data })
- sendImage 以 msg_type:'image' 发送到会话
- replyImageInThread 以 reply_in_thread 回复到话题内

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
让 agent 把工作区里的本地图片(截图/生成图)发回当前会话。
- send-image.ts:校验路径存在/大小≤10MB/magic bytes 为图片,
  上传后按 threadReplyMsgId 路由到话题或会话
- index.ts:createFeishuToolsMcpServer 增加 threadReplyMsgId 参数,
  与 main-chat 同样 gate on chatId 注册
- executor.ts:透传 input.threadRootMessageId 作为话题落点

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- send-image.test.ts:覆盖成功(会话/话题)、文件不存在/过大/为空、
  非图片格式、上传失败、发送失败、异常、jpg 识别等 11 个用例
- index.test.ts:补 send-image mock,校验 chatId 存在时注册(12 个工具)、
  缺省时不注册

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
补齐 main-chat/send-image/message-file/message-image 四个非子开关工具,
记录图片发送落点的设计决策与 im.image.create 的解包差异。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @lishuceo's task in 2m 46s —— View job


Code Review: feishu_send_image

  • Read CLAUDE.md & PR context
  • Check for previous review comments (none found)
  • Review client.ts (upload/send/reply methods)
  • Review send-image.ts (MCP tool + validation)
  • Review wiring (tools/index.ts, executor.ts)
  • Review tests

✅ Approved — clean, well-scoped, well-tested

This is a solid implementation of 方案 A. I verified the key risk areas and found no blocking issues.

Correctness verified:

  • uploadImage unpacked return — Confirmed against the SDK type. node-sdk types im.image.create as Promise<{ image_key?: string } | null> (no {code,msg,data} wrapper), unlike the message APIs. So resp?.image_key is correct, and the | null case is safely handled by optional chaining + the if (!imageKey) guard in the tool. The inline comment documenting this quirk is appreciated.
  • Thread routingreplyImageInThread(threadRootMessageId, …) is consistent with how replyTextInThread/replyPostInThread are called elsewhere (cron scheduler, restart notifications) using the thread root message id. ✔
  • Wiringfeishu_send_image correctly gated on chatId (mirrors feishuMainChatTool), and threadRootMessageId is threaded through executor.ts → createFeishuToolsMcpServer. Tool count test updated to 12 with chatId / absent without. ✔
  • Validation — exists → isFile → empty → size ≤ 10MB → magic-bytes is a sensible order, fails fast before upload, and every branch returns isError. The 11 test cases cover all branches. ✔

Non-blocking observations (no change required):

  1. send-image.ts:77resolve(args.file_path) resolves relative paths against the server process cwd, not the agent's isolated workspace cwd. The tool description does say "绝对路径", and a relative path just yields a clear "不存在" error, so this is benign — but if you later see agents passing workspace-relative paths, that's the reason.
  2. Supported formats (png/jpg/gif/webp/bmp) are a subset of what Feishu accepts (also TIFF/ICO per the SDK docs). Already noted as intentional scope; fine.
  3. No path sandboxing on the read — but the agent already has filesystem read access via Read/Bash, so this introduces no new capability. isInsideSourceRepo only guards writes, which is unaffected here.

Note: I couldn't execute npx vitest run in this environment (command needs approval), so I relied on static analysis plus the PR's reported green run (1696 passing). The test structure looks correct.

@lishuceo lishuceo merged commit 88bf472 into main Jun 14, 2026
4 checks passed
@lishuceo lishuceo deleted the feat/feishu-send-image branch June 14, 2026 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant