Guide to upload and send local files to Feishu users via OpenClaw by obtaining tenant token, uploading files for file_key, then sending file message using Fe...
# OpenClaw 飞书文件传输技术分享
> **作者**: SwingMonkey
>
> **日期**: 2026-03-14
>
> **适用**: OpenClaw Agent 飞书文件传输场景
---
## 一、问题背景
OpenClaw 的 `message` 工具在飞书渠道发送本地文件时,会直接发送文件路径而非上传文件内容。需要通过飞书 API 实现真正的文件上传和发送。
---
## 二、核心流程
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 获取 Token │ ──▶ │ 上传文件 │ ──▶ │ 发送消息 │
│ (Step 1) │ │ (Step 2) │ │ (Step 3) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
tenant_ file_key 消息送达
access_token (文件标识) 飞书用户
```
---
## 三、详细步骤
### Step 1: 获取 tenant_access_token
**接口**: `POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal`
**请求头**:
```
Content-Type: application/json
```
**请求体**:
```json
{
"app_id": "cli_xxxxxxxxxx",
"app_secret": "xxxxxxxxxx"
}
```
**PowerShell 实现**:
```powershell
$tokenResponse = Invoke-RestMethod `
-Uri 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal' `
-Method POST `
-Headers @{'Content-Type'='application/json'} `
-Body '{"app_id":"cli_xxxxxxxxxx","app_secret":"xxxxxxxxxx"}'
$TOKEN = $tokenResponse.tenant_access_token
```
**返回示例**:
```json
{
"code": 0,
"msg": "ok",
"tenant_access_token": "t-xxxxxxxxxx",
"expire": 7200
}
```
---
### Step 2: 上传文件获取 file_key
**接口**: `POST https://open.feishu.cn/open-apis/im/v1/files`
**关键要点**:
- 使用 **multipart/form-data** 格式
- **文件名使用英文**(避免中文编码问题)
- 必须包含 `file` 和 `file_type` 字段
**Python 实现**(推荐):
```python
import urllib.request
import json
import ssl
# SSL 配置
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
def upload_file(token, file_path, display_name):
"""
上传文件到飞书
Args:
token: tenant_access_token
file_path: 本地文件路径
display_name: 显示文件名(建议英文)
Returns:
file_key: 文件标识
"""
url = 'https://open.feishu.cn/open-apis/im/v1/files'
# 读取文件
with open(file_path, 'rb') as f:
file_data = f.read()
# 构建 multipart
boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'
body = b''
# file 字段
body += f'------{boundary}\r\n'.encode('utf-8')
body += f'Content-Disposition: form-data; name="file"; filename="{display_name}"\r\n'.encode('utf-8')
body += b'Content-Type: application/octet-stream\r\n\r\n'
body += file_data
body += b'\r\n'
# file_type 字段
body += f'------{boundary}\r\n'.encode('utf-8')
body += b'Content-Disposition: form-data; name="file_type"\r\n\r\n'
body += b'stream\r\n'
# 结束
body += f'------{boundary}--\r\n'.encode('utf-8')
# 请求
req = urllib.request.Request(url, data=body, method='POST')
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Content-Type', f'multipart/form-data; boundary=----{boundary}')
with urllib.request.urlopen(req, context=ssl_context) as response:
result = json.loads(response.read().decode('utf-8'))
return result['data']['file_key']
# 使用示例(支持中文文件名)
FILE_KEY = upload_file(
token='t-xxxxxxxxxx',
file_path='/path/to/your/file.docx',
display_name='语文.docx' # 支持中文文件名
)
```
**返回示例**:
```json
{
"code": 0,
"data": {
"file_key": "file_v3_00vp_xxxxxxxxxx"
},
"msg": "success"
}
```
---
### Step 3: 发送文件消息
**接口**: `POST https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id`
**请求头**:
```
Authorization: Bearer {tenant_access_token}
Content-Type: application/json; charset=utf-8
```
**请求体**:
```json
{
"receive_id": "ou_xxxxxxxxxx",
"msg_type": "file",
"content": "{\"file_key\":\"file_v3_00vp_xxxxxxxxxx\"}"
}
```
**PowerShell 实现**:
```powershell
$body = @{
receive_id = "ou_xxxxxxxxxx"
msg_type = "file"
content = '{"file_key":"file_v3_00vp_xxxxxxxxxx"}'
} | ConvertTo-Json
Invoke-RestMethod `
-Uri 'https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id' `
-Method POST `
-Headers @{
'Authorization' = "Bearer $TOKEN"
'Content-Type' = 'application/json; charset=utf-8'
} `
-Body $body
```
**返回示例**:
```json
{
"code": 0,
"data": {
"message_id": "om_xxxxxxxxxx",
"msg_type": "file"
},
"msg": "success"
}
```
---
## 四、完整示例代码
### Python 完整版
```python
#!/usr/bin/env python3
"""
OpenClaw 飞书文件传输工具
用法: python feishu_transfer.py <file_path> <receive_id>
"""
import urllib.request
import json
import ssl
import os
import sys
class FeishuFileTransfer:
def __init__(self, app_id, app_secret):
self.app_id = app_id
self.app_secret = app_secret
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE
def get_token(self):
"""获取 tenant_access_token"""
url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'
data = {'app_id': self.app_id, 'app_secret': self.app_secret}
req = urllib.request.Request(
url,
data=json.dumps(data).encode('utf-8'),
method='POST'
)
req.add_header('Content-Type', 'application/json')
with urllib.request.urlopen(req, context=self.ssl_context) as res:
result = json.loads(res.read().decode('utf-8'))
return result['tenant_access_token']
def upload_file(self, token, file_path, display_name=None):
"""上传文件"""
if display_name is None:
display_name = os.path.basename(file_path)
url = 'https://open.feishu.cn/open-apis/im/v1/files'
with open(file_path, 'rb') as f:
file_data = f.read()
# 构建 multipart
boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'
body = b''
body += f'------{boundary}\r\n'.encode('utf-8')
body += f'Content-Disposition: form-data; name="file"; filename="{display_name}"\r\n'.encode('utf-8')
body += b'Content-Type: application/octet-stream\r\n\r\n'
body += file_data
body += b'\r\n'
body += f'------{boundary}\r\n'.encode('utf-8')
body += b'Content-Disposition: form-data; name="file_type"\r\n\r\n'
body += b'stream\r\n'
body += f'------{boundary}--\r\n'.encode('utf-8')
req = urllib.request.Request(url, data=body, method='POST')
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Content-Type', f'multipart/form-data; boundary=----{boundary}')
with urllib.request.urlopen(req, context=self.ssl_context) as res:
result = json.loads(res.read().decode('utf-8'))
return result['data']['file_key']
def send_file(self, token, file_key, receive_id):
"""发送文件消息"""
url = 'https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id'
data = {
'receive_id': receive_id,
'msg_type': 'file',
'content': json.dumps({'file_key': file_key})
}
req = urllib.request.Request(
url,
data=json.dumps(data).encode('utf-8'),
method='POST'
)
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Content-Type', 'application/json')
with urllib.request.urlopen(req, context=self.ssl_context) as res:
return json.loads(res.read().decode('utf-8'))
# 使用示例
if __name__ == '__main__':
# 配置(替换为你的凭证)
APP_ID = 'cli_xxxxxxxxxx'
APP_SECRET = 'xxxxxxxxxx'
FILE_PATH = '/path/to/your/file.docx'
RECEIVE_ID = 'ou_xxxxxxxxxx'
# 执行传输(支持中文文件名)
transfer = FeishuFileTransfer(APP_ID, APP_SECRET)
token = transfer.get_token()
file_key = transfer.upload_file(token, FILE_PATH, '语文.docx') # 中文文件名
result = transfer.send_file(token, file_key, RECEIVE_ID)
print(f'发送成功: {result}')
```
---
## 五、关键注意事项
### 1. 文件名编码问题
- **问题**: 早期中文文件名可能显示为乱码
- **解决**: 确保 multipart 请求中 `file_name` 字段正确编码为 UTF-8
- **验证**: 中文文件名 `语文.docx` 可以正常显示
### 2. 文件大小限制
- **问题**: 不能上传空文件(大小为 0 字节)
- **解决**: 确保文件有实际内容
### 3. 编码设置
- **PowerShell**: 使用 `-ContentType 'application/json; charset=utf-8'`
- **Python**: 使用 `encode('utf-8')` 处理中文
### 4. 避免重复发送
- 确认上传成功后再执行发送步骤
- 不要多次调用发送 API
---
## 六、常见问题排查
| 问题 | 原因 | 解决 |
| --- | --- | --- |
| 文件名显示为乱码 | 编码问题 | 确保 `file_name` 字段 UTF-8 编码 |
| 上传返回 400 | 文件为空 | 确保文件有内容 |
| 发送后文件名是 UUID | 未正确设置文件名 | 在上传时指定 `filename` |
| 中文内容乱码 | 编码问题 | 使用 `charset=utf-8` |
---
## 七、参考文档
- [飞书开放平台 - 上传文件](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/file/create)
- [飞书开放平台 - 发送消息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create)
---
*本文档由 SwingMonkey 整理分享,供社区参考使用*
don't have the plugin yet? install it then click "run inline in claude" again.
send local files to feishu users through openclaw by obtaining a tenant access token from the feishu api, uploading the file to get a file_key identifier, then sending a file message to the target user. use this when openclaw's message tool only sends file paths instead of actual file content, or when you need reliable file delivery with proper metadata to feishu contacts.
feishu app credentials (required):
APP_ID: feishu app id (format: cli_xxxxxxxxxx). store in env var FEISHU_APP_IDAPP_SECRET: feishu app secret. store in env var FEISHU_APP_SECRET. never commit to version controlfile to upload (required):
FILE_PATH: absolute or relative path to local file (e.g. /path/to/document.docx)DISPLAY_NAME: optional custom filename for feishu ui (defaults to basename of FILE_PATH). use ascii/utf-8 characters only; avoid special chars that break multipart encodingrecipient identifier (required):
RECEIVE_ID: feishu user open_id (format: ou_xxxxxxxxxx). this is the target user's unique id, not their nameRECEIVE_ID_TYPE: always use "open_id" for user direct messages. other types (user_id, chat_id) require different api endpointsexternal connection:
network and ssl (edge cases):
input: APP_ID, APP_SECRET
action: call feishu auth endpoint to exchange credentials for a bearer token valid for 2 hours.
http details:
python implementation:
import urllib.request
import json
import ssl
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
def get_tenant_token(app_id, app_secret):
url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'
payload = {'app_id': app_id, 'app_secret': app_secret}
req = urllib.request.Request(
url,
data=json.dumps(payload).encode('utf-8'),
method='POST'
)
req.add_header('Content-Type', 'application/json')
with urllib.request.urlopen(req, context=ssl_context) as response:
result = json.loads(response.read().decode('utf-8'))
if result.get('code') != 0:
raise Exception(f"auth failed: {result.get('msg')}")
return result['tenant_access_token']
token = get_tenant_token(APP_ID, APP_SECRET)
powershell implementation:
$body = @{
app_id = "cli_xxxxxxxxxx"
app_secret = "xxxxxxxxxx"
} | ConvertTo-Json
$response = Invoke-RestMethod `
-Uri 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal' `
-Method POST `
-ContentType 'application/json; charset=utf-8' `
-Body $body
$TOKEN = $response.tenant_access_token
output: tenant_access_token string (format: t-xxxxxxxxxx). typical response code 0 with msg "ok". any non-zero code indicates auth failure (check APP_ID and APP_SECRET validity)
input: tenant_access_token, FILE_PATH, DISPLAY_NAME
action: upload file via multipart/form-data to feishu file api. returns a unique file_key identifier needed for step 3.
critical requirements:
http details:
python implementation (recommended):
def upload_file_to_feishu(token, file_path, display_name=None):
if display_name is None:
display_name = os.path.basename(file_path)
url = 'https://open.feishu.cn/open-apis/im/v1/files'
# read binary file data
with open(file_path, 'rb') as f:
file_data = f.read()
if len(file_data) == 0:
raise ValueError(f"file {file_path} is empty")
# build multipart body manually
boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'
body = b''
# add file field
body += f'------{boundary}\r\n'.encode('utf-8')
body += f'Content-Disposition: form-data; name="file"; filename="{display_name}"\r\n'.encode('utf-8')
body += b'Content-Type: application/octet-stream\r\n\r\n'
body += file_data
body += b'\r\n'
# add file_type field
body += f'------{boundary}\r\n'.encode('utf-8')
body += b'Content-Disposition: form-data; name="file_type"\r\n\r\n'
body += b'stream\r\n'
# end boundary
body += f'------{boundary}--\r\n'.encode('utf-8')
# send request
req = urllib.request.Request(url, data=body, method='POST')
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Content-Type', f'multipart/form-data; boundary=----{boundary}')
with urllib.request.urlopen(req, context=ssl_context) as response:
result = json.loads(response.read().decode('utf-8'))
if result.get('code') != 0:
raise Exception(f"upload failed: {result.get('msg')}")
return result['data']['file_key']
file_key = upload_file_to_feishu(token, '/path/to/file.docx', '语文.docx')
powershell implementation:
# Note: PowerShell multipart handling is verbose. Python is strongly preferred.
$filePath = "C:\path\to\file.docx"
$fileBytes = [System.IO.File]::ReadAllBytes($filePath)
# construct multipart body (simplified example)
$boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
$body = ... # omitted for brevity; see python version for full implementation
Invoke-RestMethod `
-Uri 'https://open.feishu.cn/open-apis/im/v1/files' `
-Method POST `
-Headers @{
'Authorization' = "Bearer $TOKEN"
'Content-Type' = "multipart/form-data; boundary=----$boundary"
} `
-Body $body
output: file_key string (format: file_v3_00vp_xxxxxxxxxx). typical response code 0. non-zero code with msg "invalid_parameter" suggests empty file or malformed multipart body
input: tenant_access_token, file_key, RECEIVE_ID
action: send file message containing the file_key to the target feishu user. message arrives in their direct message inbox with the original filename and download option.
http details:
python implementation:
def send_file_message(token, file_key, receive_id):
url = 'https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id'
payload = {
'receive_id': receive_id,
'msg_type': 'file',
'content': json.dumps({'file_key': file_key})
}
req = urllib.request.Request(
url,
data=json.dumps(payload).encode('utf-8'),
method='POST'
)
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Content-Type', 'application/json; charset=utf-8')
with urllib.request.urlopen(req, context=ssl_context) as response:
result = json.loads(response.read().decode('utf-8'))
if result.get('code') != 0:
raise Exception(f"send failed: {result.get('msg')}")
return result['data']['message_id']
message_id = send_file_message(token, file_key, RECEIVE_ID)
powershell implementation:
$body = @{
receive_id = "ou_xxxxxxxxxx"
msg_type = "file"
content = '{"file_key":"file_v3_00vp_xxxxxxxxxx"}'
} | ConvertTo-Json
$response = Invoke-RestMethod `
-Uri 'https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id' `
-Method POST `
-Headers @{
'Authorization' = "Bearer $TOKEN"
'Content-Type' = 'application/json; charset=utf-8'
} `
-Body $body
$MESSAGE_ID = $response.data.message_id
output: message_id string (format: om_xxxxxxxxxx) confirming successful delivery. typical response code 0. non-zero code with msg "invalid_receive_id" indicates RECEIVE_ID does not exist or is malformed
if auth fails (non-zero code from step 1):
if file upload fails (non-zero code from step 2):
if file send fails (non-zero code from step 3):
if token expires during multi-file operations:
if receiving send errors on large files:
success state:
data format:
file location:
no local artifacts (files are remote):
user-facing confirmation:
programmatic confirmation:
failure signals (skip step 3 if any occur):
recipient verification:
python standalone script:
#!/usr/bin/env python3
"""
feishu file transfer standalone script.
usage: python feishu_transfer.py <file_path> <receive_id> [display_name]
example: python feishu_transfer.py /home/user/report.xlsx ou_abc123 "Q4 Report"
"""
import urllib.request
import json
import ssl
import os
import sys
class FeishuFileTransfer:
def __init__(self, app_id, app_secret):
self.app_id = app_id
self.app_secret = app_secret
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE
def get_tenant_token(self):
"""step 1: obtain tenant_access_token"""
url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'
payload = {'app_id': self.app_id, 'app_secret': self.app_secret}
req = urllib.request.Request(
url,
data=json.dumps(payload).encode('utf-8'),
method='POST'
)
req.add_header('Content-Type', 'application/json')
try:
with urllib.request.urlopen(req, context=self.ssl_context) as response:
result = json.loads(response.read().decode('utf-8'))
if result.get('code') != 0:
raise Exception(f"auth failed: code={result.get('code')}, msg={result.get('msg')}")
print(f"[step 1] token obtained (expires in {result.get('expire')}s)")
return result['tenant_access_token']
except urllib.error.URLError as e:
raise Exception(f"network error during auth: {e}")
def upload_file(self, token, file_path, display_name=None):
"""step 2: upload file and obtain file_key"""
if display_name is None:
display_name = os.path.basename(file_path)
if not os.path.exists(file_path):
raise FileNotFoundError(f"file not found: {file_path}")
file_size = os.path.getsize(file_path)
if file_size == 0:
raise ValueError(f"file is empty: {file_path}")
url = 'https://open.feishu.cn/open-apis/im/v1/files'
with open(file_path, 'rb') as f:
file_data = f.read()
boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'
body = b''
body += f'------{boundary}\r\n'.encode('utf-8')
body += f'Content-Disposition: form-data; name="file"; filename="{display_name}"\r\n'.encode('utf-8')
body += b'Content-Type: application/octet-stream\r\n\r\n'
body += file_data
body += b'\r\n'
body += f'------{boundary}\r\n'.encode('utf-8')
body += b'Content-Disposition: form-data; name="file_type"\r\n\r\n'
body += b'stream\r\n'
body += f'------{boundary}--\r\n'.encode('utf-8')
req = urllib.request.Request(url, data=body, method='POST')
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Content-Type', f'multipart/form-data; boundary=----{boundary}')
try:
with urllib.request.urlopen(req, context=self.ssl_context) as response:
result = json.loads(response.read().decode('utf-8'))
if result.get('code') != 0:
raise Exception(f"upload failed: code={result.get('code')}, msg={result.get('msg')}")
file_key = result['data']['file_key']
print(f"[step 2] file uploaded (size={file_size}B, key={file_key})")
return file_key
except urllib.error.URLError as e:
raise Exception(f"network error during upload: {e}")
def send_file_message(self, token, file_key, receive_id):
"""step 3: send file message"""
url = 'https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id'
payload = {
'receive_id': receive_id,
'msg_type': 'file',
'content': json.dumps({'file_key': file_key})
}
req = urllib.request.Request(
url,
data=json.dumps(payload).encode('utf-8'),
method='POST'
)
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Content-Type', 'application/json; charset=utf-8')
try:
with urllib.request.urlopen(req, context=self.ssl_context) as response:
result = json.loads(response.read().decode('utf-8'))
if result.get('code') != 0:
raise Exception(f"send failed: code={result.get('code')}, msg={result.get('msg')}")
message_id = result['data']['message_id']
print(f"[step 3] message sent to {receive_id} (message_id={message_id})")
return message_id
except urllib.error.URLError as e:
raise Exception(f"network error during send: {e}")
def transfer(self, file_path, receive_id, display_name=None):
"""execute full transfer pipeline"""
print(f"starting feishu file transfer: {file_path} -> {receive_id}")
token = self.get_tenant_token()
file_key = self.upload_file(token, file_path, display_name)
message_id = self.send_file_message(token, file_key, receive_id)
print(f"transfer complete: {message_id}")
return message_id
if __name__ == '__main__':
APP_ID = os.getenv('FEISHU_APP_ID')
APP_SECRET = os.getenv('FEISHU_APP_SECRET')
if not APP_ID or not APP_SECRET:
print("error: FEISHU_APP_ID and FEISHU_APP_SECRET env vars required")
sys.exit(1)
if len(sys.argv) < 3:
print(f"usage: {sys.argv[0]} <file_path> <receive_id> [display_name]")
sys.exit(1)
file_path = sys.argv[1]
receive_id = sys.argv[2]
display_name = sys.argv[3] if len(sys.argv) > 3 else None
try:
transfer = FeishuFileTransfer(APP_ID, APP_SECRET)
transfer.transfer(file_path, receive_id, display_name)
except Exception as e:
print(f"error: {e}")
sys.exit(1)
run with:
export FEISHU_APP_ID=cli_xxxxxxxxxx
export FEISHU_APP_SECRET=xxxxxxxxxx
python feishu_transfer.py /path/to/file.docx ou_user123 "My Document"
| symptom | root cause | fix |
|---|---|---|
| filename shows as uuid or garbage | multipart encoding error or non-utf-8 chars in display_name | ensure display_name is valid utf-8; use python version if possible |
| "invalid_parameter" on upload | file is 0 bytes or multipart boundary mismatch | verify file has content; check boundary string matches exactly in headers |
| "invalid_receive_id" on send | receive_id is not open_id format or user does not exist | confirm receive_id is ou_xxxxxxxxxx format and user is active in tenant |
| "permission denied" | app lacks im:message scope | grant im:message and im:file scopes in feishu console under app permissions |
| file downloaded but appears corrupted | upload interrupted or file type incompatible | re-run upload with same file; try different file format if issue persists |
| token auth fails | credentials expired or incorrect | verify APP_ID and APP_SECRET in feishu developer console; regenerate if needed |
| slow upload for large files | network latency or rate limiting | increase timeout; split very large files (>100MB) into smaller uploads |
credits: original guide by SwingMonkey.