Skill: leileitian/feishu-file-sender-1-0-9 (version 1.0.0)

--- name: feishu-file-sender description: 飞书文件发送器 — 补齐飞书渠道缺失的文件投递能力，通过飞书 OpenAPI 上传并发送 agent 生成的本地文件。| Feishu File Sender — Upload & send files via Feishu OpenAPI, filling the channel's missing file delivery. license: MIT compatibility: openclaw metadata: version: "1.0.9" tags: [feishu, file, upload, im, messaging, openapi] author: wen-ai openclaw: emoji: "📎" requires: bins: [python3] config: - ~/.openclaw/openclaw.json --- # Feishu File Sender | 飞书文件发送器 OpenClaw agent 在本地生成文件，但飞书渠道插件只支持文本消息，没有文件投递能力。本 skill 通过直接调用飞书 OpenAPI（上传+发送）补齐这一底层能力，使生成的文件能回传到聊天中。 OpenClaw agents generate files locally, but the Feishu channel integration only supports text messages and does not provide native file delivery. This skill fills that architectural gap by calling Feishu OpenAPI directly (upload + send), enabling files to be delivered back to chat. 将本地文件上传到飞书并作为文件消息发送。 Upload a local file to Feishu and send it as a file message. ## 快速开始 | Quick Start ```bash python3 scripts/feishu_file_sender.py \ --file /absolute/path/to/report.xlsx \ --receive-id oc_xxx ``` ## 使用方法 | Usage ```bash python3 scripts/feishu_file_sender.py \ --file /absolute/path/to/file \ --receive-id <chat_id|open_id> \ --receive-id-type <chat_id|open_id|user_id> ``` ### 参数说明 | Arguments - `--file`（必填）：本地文件绝对路径。 - `--receive-id`（可选）：目标 chat_id 或 open_id。若省略，脚本会读取 `OPENCLAW_CHAT_ID`（或 `OPENCLAW_RECEIVE_ID` / `FEISHU_CHAT_ID`）。 - `--receive-id-type`（可选）：若省略，将根据前缀自动识别： - `oc_` → chat_id - `ou_` → open_id - `on_` → user_id - `--file-type`（可选）：飞书上传的文件类型，默认 `stream`。 - `--file` (required): Absolute path to the local file. - `--receive-id` (optional): Target chat_id or open_id. If omitted, the script reads `OPENCLAW_CHAT_ID` (or `OPENCLAW_RECEIVE_ID` / `FEISHU_CHAT_ID`). - `--receive-id-type` (optional): If omitted, auto-detect by prefix: - `oc_` → chat_id - `ou_` → open_id - `on_` → user_id - `--file-type` (optional): Feishu file upload type, default `stream`. ## 工作原理 | How It Works 1. 通过 `cwd` 匹配 OpenClaw 工作区，解析当前 agent id。 2. 根据 agent id 从 `~/.openclaw/openclaw.json` 读取 appId/appSecret。 3. 调用飞书 **上传文件** API 获取 `file_key`。 4. 调用飞书 **发送消息** API 发送文件。 1. Resolve the current agent id by matching `cwd` to OpenClaw workspace path. 2. Read appId/appSecret from `~/.openclaw/openclaw.json` based on the agent id. 3. Call Feishu **Upload File** API to get `file_key`. 4. Call Feishu **Send Message** API to deliver the file. ## 错误处理 | Error Handling - **缺少凭证** → 确保 `channels.feishu.accounts` 存在于 `~/.openclaw/openclaw.json`，且 bindings 映射 agentId → accountId。 - **机器人不在群内（230002）** → 将机器人加入目标群或换一个群。 - **缺少 receive_id** → 传入 `--receive-id` 或设置 `OPENCLAW_CHAT_ID`。 - **HTTP 错误** → 查看飞书错误返回中的 `log_id` 进行排查。 - **Missing credentials** → Ensure `channels.feishu.accounts` exists in `~/.openclaw/openclaw.json` and bindings map agentId → accountId. - **Bot not in chat (code 230002)** → Add the bot to the target chat or use a chat where the bot is present. - **Missing receive_id** → Pass `--receive-id` or set `OPENCLAW_CHAT_ID`. - **HTTP errors** → Check the returned `log_id` in Feishu error payload. ## 安全说明 | Security 本技能会从本机 OpenClaw 配置中读取飞书凭证（`~/.openclaw/openclaw.json`）： - `channels.feishu.accounts.*.appId` - `channels.feishu.accounts.*.appSecret` 这些凭证仅用于获取 tenant access token 并发送文件。技能不会存储或向其他地方传输凭证。 This skill reads Feishu credentials from the local OpenClaw config (`~/.openclaw/openclaw.json`) on the machine where it runs: - `channels.feishu.accounts.*.appId` - `channels.feishu.accounts.*.appSecret` These values are used only to obtain a tenant access token and send the file. The skill does not store or transmit credentials anywhere else. ## 备注 | Notes - 本技能面向 **所有 agent** 设计，会自动读取当前工作区来选择正确的 飞书应用凭证。 - 建议通过入站 `chat_id` 发送到 **当前聊天**。 - This skill is designed for **all agents**; it reads the active workspace to choose the correct Feishu app credentials automatically. - Prefer sending to the **current chat** by passing the inbound `chat_id`. ## 随附脚本 | Bundled Script - `scripts/feishu_file_sender.py`

path: clawdis.yaml
content: name: feishu-file-sender
version: 1.0.9
description: 飞书文件发送器 — 补齐飞书渠道缺失的文件投递能力，通过飞书 OpenAPI 上传并发送 agent 生成的本地文件。| Feishu File Sender — Upload & send files via Feishu OpenAPI, filling the channel's missing file delivery.
author:
  name: wen-ai
license: MIT
type: utility

runtime:
  requires_binaries: true
  requires_credentials: true
  requires_network: true

runtime_binaries:
  required:
    - python3

runtime_credentials:
  required:
    - OpenClaw config: ~/.openclaw/openclaw.json
  notes:
    - Reads channels.feishu.accounts.*.appId
    - Reads channels.feishu.accounts.*.appSecret

security:
  install_time_executes_code: false
  runtime_agent_executes_code: true
  runtime_agent_network_access: true
  reads_local_config: true

tags:
  - feishu
  - file
  - upload
  - openapi
  - messaging
  - attachment


path: README.md
content: # Feishu File Sender | 飞书文件发送器

OpenClaw agent 在本地生成文件，但飞书渠道插件只支持文本消息，没有文件投递能力。本 skill 通过直接调用飞书 OpenAPI（上传+发送）补齐这一底层能力，使生成的文件能回传到聊天中。

OpenClaw agents generate files locally, but the Feishu channel integration only supports text messages and does not provide native file delivery. This skill fills that architectural gap by calling Feishu OpenAPI directly (upload + send), enabling files to be delivered back to chat.

将本地文件上传到飞书 OpenAPI 并发送到聊天中。

Upload a local file to Feishu OpenAPI and send it into a chat.

## 为什么需要这个 skill | Why this skill

OpenClaw agent 生成文件后只能输出**本地路径**，飞书端用户无法直接看到或下载该文件。本 skill 将本地文件上传到飞书并发送为可下载的附件，解决“看不到/下不了”的问题。

OpenClaw agents can generate files, but they can only output a **local path**. In Feishu, users cannot see or download that file directly. This skill solves the gap by uploading the local file to Feishu and sending it as a downloadable attachment.

## 功能亮点 | Features

- 📎 上传本地文件并发送为飞书文件消息
- 🔑 自动从 OpenClaw 配置读取 appId/appSecret
- 🧭 基于工作区对 **所有 agent** 通用
- 🧰 简洁的命令行工具，方便快速使用

- 📎 Upload local files and send as Feishu file messages
- 🔑 Auto-resolve appId/appSecret from OpenClaw config
- 🧭 Works across **all agents** based on workspace
- 🧰 Simple CLI for quick use

## 运行要求 | Requirements

- Python 3.6+
- 已安装 `requests`
- OpenClaw 已配置飞书渠道

- Python 3.6+
- `requests` installed
- OpenClaw with Feishu channel configured

## 安装 | Install

```bash
python3 -m pip install requests
```

## 用法 | Usage

### 发送到当前聊天（推荐） | Send to current chat (recommended)

```bash
# 如果运行环境通过环境变量提供 chat id
export OPENCLAW_CHAT_ID=oc_xxx

python3 scripts/feishu_file_sender.py \
  --file /absolute/path/to/report.xlsx
```

### 发送到指定聊天 | Send to a specific chat

```bash
python3 scripts/feishu_file_sender.py \
  --file /absolute/path/to/report.xlsx \
  --receive-id oc_xxx \
  --receive-id-type chat_id
```

### 发送给指定用户 | Send to a user

```bash
python3 scripts/feishu_file_sender.py \
  --file /absolute/path/to/report.xlsx \
  --receive-id ou_xxx \
  --receive-id-type open_id
```

## 工作原理 | How It Works

1. 通过 `cwd` 匹配配置的工作区，解析当前 agent id。
2. 通过绑定关系从 `~/.openclaw/openclaw.json` 读取 Feishu `appId/appSecret`。
3. 上传文件到飞书（`im/v1/files`），获取 `file_key`。
4. 调用消息发送接口（`im/v1/messages`）发送到目标聊天/用户。

1. Resolve current agent id by matching `cwd` to the configured workspace.
2. Read Feishu `appId/appSecret` from `~/.openclaw/openclaw.json` via bindings.
3. Upload the file to Feishu (`im/v1/files`) and get `file_key`.
4. Send a file message (`im/v1/messages`) to the target chat/user.

## 常见错误处理 | Error Handling

| 问题 | 原因 | 解决办法 |
|------|------|---------|
| `Missing receive_id` | 未传 `--receive-id` 且无环境变量 | 设置 `OPENCLAW_CHAT_ID` 或传入 `--receive-id` |
| `No Feishu account binding` | 缺少 agent 绑定 | 确保 OpenClaw 配置中 agentId → accountId 绑定存在 |
| `Bot/User can NOT be out of the chat (230002)` | 机器人不在群内 | 将机器人加入群或发送到其他群 |
| `HTTPError` | API 调用失败 | 查看响应 `log_id` 与飞书排障链接 |

| Issue | Cause | Fix |
|------|------|-----|
| `Missing receive_id` | No `--receive-id` and no env | Set `OPENCLAW_CHAT_ID` or pass `--receive-id` |
| `No Feishu account binding` | Agent binding missing | Ensure bindings map agentId → accountId in OpenClaw config |
| `Bot/User can NOT be out of the chat (230002)` | Bot not in chat | Add the bot to the chat or send to a different chat |
| `HTTPError` | API failure | Check response `log_id` and Feishu troubleshooting link |

## 配置说明 | Configuration

OpenClaw 应已在 `~/.openclaw/openclaw.json` 中配置飞书账号。本技能只**读取**配置，不会修改任何文件。

OpenClaw should already have Feishu accounts configured in `~/.openclaw/openclaw.json`. This skill only **reads** config; it does not modify any files.

## 安全说明 | Security

本技能会从本机 OpenClaw 配置中读取飞书凭证（`~/.openclaw/openclaw.json`）：

- `channels.feishu.accounts.*.appId`
- `channels.feishu.accounts.*.appSecret`

这些凭证仅用于获取 tenant access token 并发送文件。技能不会存储或向其他地方传输凭证。

This skill reads Feishu credentials from your local OpenClaw config (`~/.openclaw/openclaw.json`):

- `channels.feishu.accounts.*.appId`
- `channels.feishu.accounts.*.appSecret`

These values are used only to obtain a tenant access token and send the file. The skill does not store or transmit credentials anywhere else.

## 许可证 | License

MIT


path: scripts/feishu_file_sender.py
content: #!/usr/bin/env python3
import argparse
import json
import os
from pathlib import Path
from typing import Any, Dict, Optional, Tuple

import requests


FEISHU_TOKEN_URL = (
    "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
)
FEISHU_UPLOAD_URL = "https://open.feishu.cn/open-apis/im/v1/files"
FEISHU_SEND_MSG_URL = "https://open.feishu.cn/open-apis/im/v1/messages"
OPENCLAW_CONFIG = Path.home() / ".openclaw" / "openclaw.json"


def load_openclaw_config() -> Dict[str, Any]:
    if not OPENCLAW_CONFIG.exists():
        raise FileNotFoundError(f"OpenClaw config not found: {OPENCLAW_CONFIG}")
    return json.loads(OPENCLAW_CONFIG.read_text(encoding="utf-8"))


def resolve_agent_id(config: Dict[str, Any]) -> str:
    cwd = Path.cwd().resolve()
    best_match = (0, None)
    for agent in config.get("agents", {}).get("list", []):
        workspace = agent.get("workspace")
        agent_id = agent.get("id")
        if not workspace or not agent_id:
            continue
        workspace_path = Path(workspace).resolve()
        if str(cwd).startswith(str(workspace_path)):
            match_len = len(str(workspace_path))
            if match_len > best_match[0]:
                best_match = (match_len, agent_id)
    if best_match[1]:
        return best_match[1]
    raise RuntimeError("Unable to resolve agent id from workspace path")


def resolve_feishu_account(
    config: Dict[str, Any], agent_id: str
) -> Tuple[str, str]:
    bindings = config.get("bindings", [])
    account_id = None
    for binding in bindings:
        if binding.get("agentId") == agent_id:
            account_id = binding.get("match", {}).get("accountId")
            if account_id:
                break
    if not account_id:
        raise RuntimeError(f"No Feishu account binding for agent: {agent_id}")

    accounts = (
        config.get("channels", {})
        .get("feishu", {})
        .get("accounts", {})
    )
    account = accounts.get(account_id)
    if not account:
        raise RuntimeError(f"Feishu account not found: {account_id}")
    app_id = account.get("appId")
    app_secret = account.get("appSecret")
    if not app_id or not app_secret:
        raise RuntimeError(f"Missing appId/appSecret for account: {account_id}")
    return app_id, app_secret


def get_tenant_access_token(app_id: str, app_secret: str) -> str:
    resp = requests.post(
        FEISHU_TOKEN_URL,
        json={"app_id": app_id, "app_secret": app_secret},
        timeout=15,
    )
    resp.raise_for_status()
    data = resp.json()
    if data.get("code") != 0:
        raise RuntimeError(f"Get token failed: {data}")
    return data["tenant_access_token"]


def upload_file(token: str, file_path: Path, file_type: str) -> str:
    headers = {"Authorization": f"Bearer {token}"}
    with file_path.open("rb") as f:
        files = {"file": (file_path.name, f)}
        data = {
            "file_type": file_type,
            "file_name": file_path.name,
        }
        resp = requests.post(
            FEISHU_UPLOAD_URL,
            headers=headers,
            data=data,
            files=files,
            timeout=30,
        )
    resp.raise_for_status()
    data = resp.json()
    if data.get("code") != 0:
        raise RuntimeError(f"Upload failed: {data}")
    return data["data"]["file_key"]


def send_file_message(
    token: str,
    receive_id: str,
    receive_id_type: str,
    file_key: str,
) -> Dict[str, Any]:
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json; charset=utf-8",
    }
    params = {"receive_id_type": receive_id_type}
    payload = {
        "receive_id": receive_id,
        "msg_type": "file",
        "content": json.dumps({"file_key": file_key}),
    }
    resp = requests.post(
        FEISHU_SEND_MSG_URL,
        headers=headers,
        params=params,
        json=payload,
        timeout=15,
    )
    resp.raise_for_status()
    data = resp.json()
    if data.get("code") != 0:
        raise RuntimeError(f"Send message failed: {data}")
    return data


def infer_receive_id_type(receive_id: str, explicit: Optional[str]) -> str:
    if explicit:
        return explicit
    if receive_id.startswith("oc_"):
        return "chat_id"
    if receive_id.startswith("ou_"):
        return "open_id"
    if receive_id.startswith("on_"):
        return "user_id"
    return "chat_id"


def resolve_receive_id(cli_value: Optional[str]) -> str:
    if cli_value:
        return cli_value
    env_value = (
        os.getenv("OPENCLAW_CHAT_ID")
        or os.getenv("OPENCLAW_RECEIVE_ID")
        or os.getenv("FEISHU_CHAT_ID")
    )
    if env_value:
        return env_value
    raise RuntimeError(
        "Missing receive_id. Provide --receive-id or set OPENCLAW_CHAT_ID."
    )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Upload file to Feishu and send")
    parser.add_argument("--file", required=True, help="Local file path")
    parser.add_argument("--receive-id", default=None, help="chat_id or open_id")
    parser.add_argument(
        "--receive-id-type",
        default=None,
        help="chat_id / open_id / user_id (auto-detect if omitted)",
    )
    parser.add_argument(
        "--file-type",
        default="stream",
        help="file_type for upload, default stream",
    )
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    file_path = Path(args.file)
    if not file_path.exists():
        raise FileNotFoundError(f"File not found: {file_path}")

    config = load_openclaw_config()
    agent_id = resolve_agent_id(config)
    app_id, app_secret = resolve_feishu_account(config, agent_id)

    receive_id = resolve_receive_id(args.receive_id)
    receive_id_type = infer_receive_id_type(receive_id, args.receive_id_type)

    [REDACTED_SECRET], app_secret)
    file_key = upload_file(token, file_path, args.file_type)
    result = send_file_message(token, receive_id, receive_id_type, file_key)
    print("Send success:", result)


if __name__ == "__main__":
    main()


path: _meta.json
content: ownerId: kn72q2tgcjpqxx3psa36ewpp6d81q1zz
slug: feishu-file-sender
version: 1.0.9
publishedAt: 1772047314694

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector:  ()