深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
中
ESC

OpenClaw + Feishu Image Sending: Fixing Local Images That Won't Display

English 阅读中文版

Background

While using OpenClaw + the Feishu bot, I ran into a thorny problem:

When OpenClaw sends an image message via the Feishu callback, the image won’t display.

The reason is that OpenClaw passes Feishu a local file path:

/home/wenjun/.openclaw/media/browser/48f5c6ca-eeea-4f19-a8e9-d98f61d632e2.png

But the Feishu server can’t access files on the local machine, so the image won’t show. Feishu requires the image to be either:

  1. A publicly accessible URL
  2. Or an image_key obtained after uploading to the Feishu server

The solution

By calling Feishu’s upload image API, we first upload the local image to Feishu to obtain an image_key, then use that image_key to send the message.

The overall flow:

Local image → upload to Feishu → get image_key → send image message

Technical implementation

1. Feishu API call flow

Three steps are needed:

Step API Description
Get token POST /auth/v3/tenant_access_token/internal Use App ID and Secret to obtain an access token
Upload image POST /im/v1/images Upload the local image, get an image_key
Send message POST /im/v1/messages Send the image message using the image_key

2. Go implementation

To make integration into OpenClaw easy, I wrote a command-line tool in Go, compiled into a standalone binary.

Core code structure:

// FeishuClient the Feishu client
type FeishuClient struct {
AppID string
AppSecret string
TenantAccessToken string
}

// Get tenant_access_token
func (c *FeishuClient) GetTenantAccessToken() error {
url := "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
payload := map[string]string{
"app_id": c.AppID,
"app_secret": c.AppSecret,
}
// ... send the request and parse the response
}

// Upload the image to get image_key
func (c *FeishuClient) UploadImage(imagePath string) (string, error) {
url := "https://open.feishu.cn/open-apis/im/v1/images"

// Build multipart/form-data
var body bytes.Buffer
writer := multipart.NewWriter(&body)
_ = writer.WriteField("image_type", "message")
part, _ := writer.CreateFormFile("image", filepath.Base(imagePath))
_, _ = io.Copy(part, file)
writer.Close()

// Send the request and return the image_key
}

// Send the image message
func (c *FeishuClient) SendImageMessage(chatID, imageKey string) error {
url := "https://open.feishu.cn/open-apis/im/v1/messages"
content := map[string]string{"image_key": imageKey}
// ... build and send the message
}

3. Full code

package main

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)

// FeishuClient the Feishu client
type FeishuClient struct {
AppID string
AppSecret string
TenantAccessToken string
}

// TokenResponse the response for fetching the token
type TokenResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
TenantAccessToken string `json:"tenant_access_token"`
Expire int `json:"expire"`
}

// ImageUploadResponse the response for uploading the image
type ImageUploadResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
ImageKey string `json:"image_key"`
} `json:"data"`
}

// MessageResponse the response for sending the message
type MessageResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
}

// NewFeishuClient creates a Feishu client
func NewFeishuClient(appID, appSecret string) *FeishuClient {
return &FeishuClient{
AppID: appID,
AppSecret: appSecret,
}
}

// GetTenantAccessToken fetches tenant_access_token
func (c *FeishuClient) GetTenantAccessToken() error {
url := "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"

payload := map[string]string{
"app_id": c.AppID,
"app_secret": c.AppSecret,
}

jsonData, _ := json.Marshal(payload)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("请求 token 失败: %v", err)
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)

var result TokenResponse
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("解析 token 响应失败: %v", err)
}

if result.Code != 0 {
return fmt.Errorf("获取 token 失败: %s", result.Msg)
}

c.TenantAccessToken = result.TenantAccessToken
return nil
}

// UploadImage uploads the image to get image_key
func (c *FeishuClient) UploadImage(imagePath string) (string, error) {
if c.TenantAccessToken == "" {
if err := c.GetTenantAccessToken(); err != nil {
return "", err
}
}

url := "https://open.feishu.cn/open-apis/im/v1/images"

// Open the file
file, err := os.Open(imagePath)
if err != nil {
return "", fmt.Errorf("打开图片失败: %v", err)
}
defer file.Close()

// Create the multipart form
var body bytes.Buffer
writer := multipart.NewWriter(&body)

// Add the image_type field
_ = writer.WriteField("image_type", "message")

// Add the file
part, err := writer.CreateFormFile("image", filepath.Base(imagePath))
if err != nil {
return "", fmt.Errorf("创建 form file 失败: %v", err)
}

_, err = io.Copy(part, file)
if err != nil {
return "", fmt.Errorf("复制文件内容失败: %v", err)
}

writer.Close()

// Create the request
req, err := http.NewRequest("POST", url, &body)
if err != nil {
return "", fmt.Errorf("创建请求失败: %v", err)
}

req.Header.Set("Authorization", "Bearer "+c.TenantAccessToken)
req.Header.Set("Content-Type", writer.FormDataContentType())

// Send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("上传图片请求失败: %v", err)
}
defer resp.Body.Close()

respBody, _ := io.ReadAll(resp.Body)

var result ImageUploadResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("解析上传响应失败: %v", err)
}

if result.Code != 0 {
// Check whether it's a permission error
if result.Code == 99991672 {
return "", fmt.Errorf("权限不足: %s\n请访问 https://open.feishu.cn/app/%s/auth 开通权限: im:resource, im:message:send", result.Msg, c.AppID)
}
return "", fmt.Errorf("上传图片失败 [%d]: %s", result.Code, result.Msg)
}

return result.Data.ImageKey, nil
}

// SendImageMessage sends the image message
func (c *FeishuClient) SendImageMessage(chatID, imageKey string) error {
if c.TenantAccessToken == "" {
if err := c.GetTenantAccessToken(); err != nil {
return err
}
}

url := "https://open.feishu.cn/open-apis/im/v1/messages"

// Build the message content
content := map[string]string{
"image_key": imageKey,
}
contentBytes, _ := json.Marshal(content)

payload := map[string]interface{}{
"receive_id": chatID,
"msg_type": "image",
"content": string(contentBytes),
}

jsonData, _ := json.Marshal(payload)

// Create the request
req, err := http.NewRequest("POST", url+"?receive_id_type=chat_id", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("创建消息请求失败: %v", err)
}

req.Header.Set("Authorization", "Bearer "+c.TenantAccessToken)
req.Header.Set("Content-Type", "application/json")

// Send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("发送消息请求失败: %v", err)
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)

var result MessageResponse
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("解析消息响应失败: %v", err)
}

if result.Code != 0 {
return fmt.Errorf("发送消息失败: %s", result.Msg)
}

return nil
}

func main() {
var (
appID = flag.String("app-id", "", "Feishu App ID")
appSecret = flag.String("app-secret", "", "Feishu App Secret")
chatID = flag.String("chat-id", "", "Feishu group chat ID (chat_id)")
imagePath = flag.String("image", "", "local image path")
getKey = flag.Bool("get-key", false, "only get image_key, don't send the message")
)
flag.Parse()

// Validate parameters
if *appID == "" || *appSecret == "" || *imagePath == "" {
fmt.Fprintf(os.Stderr, "用法: %s -app-id <app_id> -app-secret <app_secret> -image <path> [-chat-id <chat_id>]\n", os.Args[0])
flag.PrintDefaults()
os.Exit(1)
}

// If we're sending a message but there's no chat_id, error out
if !*getKey && *chatID == "" {
fmt.Fprintf(os.Stderr, "错误: 发送消息需要提供 -chat-id 参数,或使用 -get-key 仅获取 image_key\n")
os.Exit(1)
}

// Create the client
client := NewFeishuClient(*appID, *appSecret)

// Upload the image
fmt.Printf("正在上传图片: %s\n", *imagePath)
imageKey, err := client.UploadImage(*imagePath)
if err != nil {
fmt.Fprintf(os.Stderr, "上传图片失败: %v\n", err)
os.Exit(1)
}

fmt.Printf("图片上传成功,image_key: %s\n", imageKey)

// If we only need the key, stop here
if *getKey {
fmt.Println(imageKey)
os.Exit(0)
}

// Send the message
fmt.Printf("正在发送消息到群聊: %s\n", *chatID)
if err := client.SendImageMessage(*chatID, imageKey); err != nil {
fmt.Fprintf(os.Stderr, "发送消息失败: %v\n", err)
os.Exit(1)
}

fmt.Println("图片消息发送成功!")
}

4. Compiling and using

# Compile (static linking, no dependencies)
go build -ldflags="-s -w" -o feishu-image-sender .

# Usage example
./feishu-image-sender \
-app-id "cli_xxxxxxxxxxxxxxxx" \
-app-secret "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-chat-id "oc_xxxxxxxxxxxxxxxx" \
-image "/path/to/image.png"

5. Results

The command line runs successfully:

The Feishu group chat receives the image:

Integrating into OpenClaw

1. Place the tool

Put the compiled binary into OpenClaw’s tools directory:

mkdir -p ~/.openclaw/tools
cp feishu-image-sender ~/.openclaw/tools/
chmod +x ~/.openclaw/tools/feishu-image-sender

2. Create a wrapper script (auto-reads config)

For convenience, create a wrapper script that automatically reads the OpenClaw Feishu config:

#!/bin/bash
# ~/.openclaw/tools/send-image-to-feishu

CONFIG_FILE="$HOME/.openclaw/openclaw.json"

# Extract the Feishu config
APP_ID=$(grep -o '"appId": "[^"]*"' "$CONFIG_FILE" | head -1 | cut -d'"' -f4)
APP_SECRET=$(grep -o '"appSecret": "[^"]*"' "$CONFIG_FILE" | head -1 | cut -d'"' -f4)

# If no chat_id is provided, automatically fetch the first group chat
if [ -z "$CHAT_ID" ]; then
TOKEN=$(curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \
-H "Content-Type: application/json" \
-d "{\"app_id\":\"$APP_ID\",\"app_secret\":\"$APP_SECRET\"}" | grep -o '"tenant_access_token":"[^"]*"' | cut -d'"' -f4)

CHAT_ID=$(curl -s -X GET "https://open.feishu.cn/open-apis/im/v1/chats" \
-H "Authorization: Bearer $TOKEN" | grep -o '"chat_id":"[^"]*"' | head -1 | cut -d'"' -f4)
fi

# Execute the send
~/.openclaw/tools/feishu-image-sender \
-app-id "$APP_ID" \
-app-secret "$APP_SECRET" \
-chat-id "$CHAT_ID" \
-image "$1"

3. Define the tool in the Skill

Add to the Skill’s TOOLS.md:

## send_feishu_image

Send a local image to a Feishu chat group.

### Parameters
- `image_path`: Path to local image file

### Command
```bash
~/.openclaw/tools/send-image-to-feishu -image "{{image_path}}"

Example

When user says “send this image to the Feishu group”:

~/.openclaw/tools/send-image-to-feishu -image "/home/xxx/.openclaw/media/xxx.png"

## Feishu permission configuration

Before using it, make sure the Feishu app has the following permissions:

1. `im:resource` or `im:resource:upload` - upload image resources
2. `im:message:send` - send messages
3. `im:chat:readonly` - get group chat info

Enable them in [Feishu Open Platform](https://open.feishu.cn/app) → your app → Permission Management.

## Summary

By writing a simple Go command-line tool, we solved the problem of local images failing to send in the OpenClaw + Feishu scenario. The whole process involved:

1. Understanding Feishu's image-message mechanism (it requires an image_key)
2. Calling Feishu's image upload API
3. Compiling into a standalone binary for easy integration
4. Creating a wrapper script that auto-reads the OpenClaw config

In the end, we achieved the convenient experience of **sending a local image to a Feishu group chat with a single command**.

The full code is open-sourced: [GitHub repo link]

---

**Related reading:**
- [Blog Comment Feishu Approval Bot: Real-Time Comment Notifications and One-Click Approval](/2026-博客评论飞书审批机器人-实现留言实时通知与一键审批/)
- [Feishu Open Platform docs - Upload Image](https://open.feishu.cn/document/server-docs/im-v1/image/create)
文章作者:阿文
文章链接: https://www.awen.me/post/df9de5a9.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客!
本文于 2026-03-02 15:00 发布,已超过半年(228天),请注意甄别内容是否已过期。

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️