Skill: ziwenwang28/feishu-proactive-messenger (version 1.0.1)

--- name: feishu-proactive-messenger description: 飞书主动消息发送器 — 飞书渠道只支持被动回复，agent 无法主动发起对话。本 skill 调用飞书 OpenAPI 发送文本消息，补齐主动投递能力。| Feishu Proactive Messenger — Send text messages proactively via Feishu OpenAPI, filling the channel's missing outbound messaging. license: MIT compatibility: openclaw metadata: version: "1.0.1" tags: [feishu, message, proactive, outbound, openapi, messaging] author: wen-ai openclaw: emoji: "📨" requires: bins: [python3] config: - ~/.openclaw/openclaw.json --- # Feishu Proactive Messenger | 飞书主动消息发送器 飞书渠道只支持被动回复——用户先发消息，agent 才能回复。当 agent 需要**主动发起对话**（如 Agent A 派任务给 Agent B，Agent B 需要在自己的飞书窗口回复用户），飞书渠道没有这个能力。本 skill 通过直接调用飞书 OpenAPI 发送文本消息，补齐主动投递能力。 The Feishu channel only supports passive replies — the user must send a message first before the agent can respond. When an agent needs to **proactively initiate a conversation** (e.g. Agent A dispatches a task to Agent B, and Agent B needs to reply in its own Feishu chat window), the channel lacks this capability. This skill fills that gap by calling Feishu OpenAPI to send text messages directly. ## 快速开始 | Quick Start ```bash python3 scripts/feishu_proactive_messenger.py --agent <agent_id> --text "Mission accomplished" ``` `--agent` 指定当前 agent 的 id（如 `coder`、`data`、`life`），脚本会自动读取对应的飞书凭证和 `defaultTo` 目标用户。若省略 `--agent`，则通过 cwd 自动匹配。 `--agent` specifies the current agent's id (e.g. `coder`, `data`, `life`). The script reads the corresponding Feishu credentials and `defaultTo` target automatically. If `--agent` is omitted, the script resolves the agent by matching cwd. ## 使用方法 | Usage ```bash python3 scripts/feishu_proactive_messenger.py \ --agent <agent_id> \ --text "要发送的消息内容" \ --receive-id <chat_id|open_id> \ --receive-id-type <chat_id|open_id|user_id> ``` ### 参数说明 | Arguments - `--agent`（推荐）：agent id（如 `coder`、`data`、`main`）。用于确定使用哪个飞书应用的凭证。若省略，通过 cwd 自动匹配（在被派发任务场景下可能不准确，建议显式指定）。 - `--text`（必填）：要发送的消息文本。 - `--receive-id`（可选）：目标 chat_id 或 open_id。若省略，依次从环境变量 `OPENCLAW_CHAT_ID` / `OPENCLAW_RECEIVE_ID` / `FEISHU_CHAT_ID` 或 account 的 `defaultTo` 配置中读取。 - `--receive-id-type`（可选）：若省略，根据前缀自动识别： - `oc_` → chat_id - `ou_` → open_id - `on_` → user_id - `--agent` (recommended): Agent id (e.g. `coder`, `data`, `main`). Determines which Feishu app credentials to use. If omitted, resolves by matching cwd (may be inaccurate in dispatched-task scenarios; explicit is recommended). - `--text` (required): The message text to send. - `--receive-id` (optional): Target chat_id or open_id. If omitted, reads from env `OPENCLAW_CHAT_ID` / `OPENCLAW_RECEIVE_ID` / `FEISHU_CHAT_ID`, or from the account's `defaultTo` config. - `--receive-id-type` (optional): If omitted, auto-detect by prefix: - `oc_` → chat_id - `ou_` → open_id - `on_` → user_id ## 工作原理 | How It Works 1. 通过 `--agent` 参数或 `cwd` 匹配确定当前 agent id。 2. 根据 agent id 从 `~/.openclaw/openclaw.json` 读取对应 account 的 appId/appSecret。 3. 从同一 account 的 `defaultTo` 读取默认目标用户（如未通过参数指定）。 4. 获取 tenant access token。 5. 通过飞书 `bot/v3/info` API 获取 bot 显示名称。 6. 调用飞书 **发送消息** API（`im/v1/messages`）发送文本消息。 7. 输出简洁结果：`✅ [Bot名称] 消息已发送`。 1. Determine agent id via `--agent` parameter or by matching `cwd`. 2. Read appId/appSecret from `~/.openclaw/openclaw.json` based on the agent id. 3. Read the default target user from the same account's `defaultTo` (if not specified via args). 4. Obtain a tenant access token. 5. Retrieve the bot's display name via Feishu `bot/v3/info` API. 6. Call Feishu **Send Message** API (`im/v1/messages`) to deliver the text message. 7. Output clean result: `✅ [BotName] 消息已发送`. ## 前置配置 | Prerequisites 每个飞书 account 需要配置 `defaultTo`，指向目标用户的 open_id： Each Feishu account needs a `defaultTo` pointing to the target user's open_id: ```bash openclaw config set channels.feishu.accounts.<account>.defaultTo "user:ou_xxx" ``` 注意：飞书的 open_id 是按应用隔离的，同一个用户在不同 bot 下有不同的 open_id。 Note: Feishu open_id is app-scoped — the same user has different open_ids under different bots. ## 错误处理 | Error Handling - **缺少凭证** → 确保 `channels.feishu.accounts` 存在于 `~/.openclaw/openclaw.json`，且 bindings 映射 agentId → accountId。 - **机器人不在聊天中（230002）** → 用户需要先跟该 bot 发起过对话。 - **缺少 receive_id** → 传入 `--receive-id`，设置 `OPENCLAW_CHAT_ID`， 或配置 `defaultTo`。 - **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)** → The user must have initiated a chat with the bot at least once. - **Missing receive_id** → Pass `--receive-id`, set `OPENCLAW_CHAT_ID`, or configure `defaultTo`. - **HTTP errors** → Check the returned `log_id` in Feishu error payload. ## 安全说明 | Security 本技能从 `~/.openclaw/openclaw.json` 读取飞书凭证： - `channels.feishu.accounts.*.appId` - `channels.feishu.accounts.*.appSecret` 凭证仅用于获取 tenant access token 并发送消息。技能不会存储或向其他地方传输凭证。 This skill reads Feishu credentials from `~/.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 message. The skill does not store or transmit credentials anywhere else. ## 备注 | Notes - 本技能面向 **所有 agent** 设计，通过 `--agent` 参数或工作区匹配选择正确的飞书应用凭证。 - 配合 `defaultTo` 使用时，agent 无需知道任何 ID 即可主动发消息。 - 与 `feishu-file-sender` 互补：一个发文件，一个发文本。 - Designed for **all agents**; uses `--agent` parameter or workspace matching to choose credentials. - When used with `defaultTo`, agents can send messages without knowing any IDs. - Complements `feishu-file-sender`: one sends files, the other sends text. ## 随附脚本 | Bundled Script - `scripts/feishu_proactive_messenger.py` ## 更新日志 | Changelog ### 1.0.1 - 新增 `--agent` 参数，显式指定 agent 身份（解决被派发任务时 cwd 匹配不准确的问题） - 新增通过飞书 `bot/v3/info` API 获取 bot 显示名称 - 输出简化为 `✅ [Bot名称] 消息已发送`，不再暴露 open_id 等敏感信息 - `resolve_agent_id` 支持 `agents.defaults.workspace` 作为 fallback ### 1.0.0 - 初始版本

path: README.md
content: # Feishu Proactive Messenger | 飞书主动消息发送器

飞书渠道只支持被动回复——用户先发消息，agent 才能回复。当 agent 需要**主动发起对话**（如 Agent A 派任务给 Agent B，Agent B 需要在自己的飞书窗口回复用户），飞书渠道没有这个能力。本 skill 通过直接调用飞书 OpenAPI 发送文本消息，补齐主动投递能力。

The Feishu channel only supports passive replies — the user must message first. When an agent needs to **proactively initiate a conversation** (e.g. Agent A dispatches a task to Agent B, and Agent B needs to reply in its own Feishu chat window), the channel lacks this capability. This skill fills that gap by calling Feishu OpenAPI directly.

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

OpenClaw 的飞书渠道是被动模式：用户发消息 → agent 回复。但在多 agent 场景下，Agent A 派任务给 Agent B 后，Agent B 需要通过**自己的飞书窗口**主动给用户发消息。没有本 skill，Agent B 只能在 Agent A 的窗口里回复，用户在 Agent B 的聊天窗口看不到任何消息。

OpenClaw's Feishu channel is passive: user sends → agent replies. In multi-agent setups, when Agent A dispatches a task to Agent B, Agent B needs to send a message in **its own Feishu chat window**. Without this skill, Agent B can only reply in Agent A's window — the user sees nothing in Agent B's chat.

## 功能亮点 | Features

- 📨 agent 可主动发送飞书文本消息，无需用户先发消息
- 🔑 自动从 OpenClaw 配置读取 appId/appSecret
- 🎯 自动从 `defaultTo` 读取目标用户，无需手动传 ID
- 🏷️ 自动获取飞书 bot 显示名称，输出清晰的发送结果
- 🧭 通过 `--agent` 参数或工作区匹配，对 **所有 agent** 通用
- 🧰 简洁的命令行工具

- 📨 Agents can proactively send Feishu text messages without user initiation
- 🔑 Auto-resolve appId/appSecret from OpenClaw config
- 🎯 Auto-read target user from `defaultTo` — no manual IDs needed
- 🏷️ Auto-fetch bot display name from Feishu for clean output
- 🧭 Works across **all agents** via `--agent` parameter or workspace matching
- 🧰 Simple CLI tool

## 运行要求 | Requirements

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

- Python 3.6+
- `requests` installed
- OpenClaw with Feishu channel configured
- Each account has `defaultTo` configured

## 安装 | Install

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

## 用法 | Usage

### 推荐用法 | Recommended usage

```bash
python3 scripts/feishu_proactive_messenger.py --agent <agent_id> --text "Mission accomplished"
```

`--agent` 指定当前 agent 的 id（如 `coder`、`data`、`life`），确保使用正确的飞书应用凭证。

### 指定目标 | Specify target

```bash
python3 scripts/feishu_proactive_messenger.py \
  --agent <agent_id> \
  --text "Mission accomplished" \
  --receive-id ou_xxx \
  --receive-id-type open_id
```

### 输出示例 | Output example

```
✅ [Wen·程序员] 消息已发送
```

## 前置配置 | Prerequisites

为每个飞书 account 配置 `defaultTo`（只需做一次）：

Set `defaultTo` for each Feishu account (one-time setup):

```bash
openclaw config set channels.feishu.accounts.main.defaultTo "user:ou_xxx"
openclaw config set channels.feishu.accounts.agent-b.defaultTo "user:ou_yyy"
# ... 其他 account
openclaw gateway restart
```

获取 open_id 的方法：给每个 bot 发一条消息，然后查日志：

How to get open_ids: send a message to each bot, then check logs:

```bash
openclaw logs --limit 300 | grep "ou_"
```

注意：飞书 open_id 是按应用隔离的，同一用户在不同 bot 下有不同的 open_id。

Note: Feishu open_id is app-scoped — same user gets different open_ids per bot.

## 工作原理 | How It Works

1. 通过 `--agent` 参数或 `cwd` 匹配确定当前 agent id。
2. 通过绑定关系从 `~/.openclaw/openclaw.json` 读取 Feishu appId/appSecret。
3. 从同一 account 的 `defaultTo` 读取默认目标（去掉 `user:` 前缀）。
4. 获取 tenant access token。
5. 通过飞书 `bot/v3/info` API 获取 bot 显示名称。
6. 调用消息发送接口（`im/v1/messages`）发送文本消息。
7. 输出 `✅ [Bot名称] 消息已发送`。

1. Determine agent id via `--agent` parameter or by matching `cwd`.
2. Read Feishu appId/appSecret from `~/.openclaw/openclaw.json` via bindings.
3. Read default target from the same account's `defaultTo` (strip `user:` prefix).
4. Obtain tenant access token.
5. Retrieve bot display name via Feishu `bot/v3/info` API.
6. Send a text message via `im/v1/messages`.
7. Output `✅ [BotName] 消息已发送`.

## 常见错误处理 | Error Handling

| 问题 | 原因 | 解决办法 |
|------|------|---------|
| `Missing receive_id` | 未传 `--receive-id` 且无 `defaultTo` | 配置 `defaultTo` 或传入 `--receive-id` |
| `No Feishu account binding` | 缺少 agent 绑定 | 确保 OpenClaw 配置中 agentId → accountId 绑定存在 |
| `Bot/User can NOT be out of the chat (230002)` | 用户未跟该 bot 发起过对话 | 先在飞书上给该 bot 发一条消息 |
| `HTTPError` | API 调用失败 | 查看响应 `log_id` 与飞书排障链接 |

| Issue | Cause | Fix |
|------|------|-----|
| `Missing receive_id` | No `--receive-id` and no `defaultTo` | Configure `defaultTo` 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)` | User never chatted with bot | Send a message to the bot in Feishu first |
| `HTTPError` | API failure | Check response `log_id` and Feishu troubleshooting link |

## 安全说明 | Security

本技能从 `~/.openclaw/openclaw.json` 读取飞书凭证：

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

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

This skill reads Feishu credentials from `~/.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 message.
The skill does not store or transmit credentials anywhere else.

## 更新日志 | Changelog

### 1.0.1
- 新增 `--agent` 参数，显式指定 agent 身份（解决被派发任务时 cwd 匹配不准确的问题）
- 新增通过飞书 `bot/v3/info` API 获取 bot 显示名称
- 输出简化为 `✅ [Bot名称] 消息已发送`，不再暴露敏感信息
- `resolve_agent_id` 支持 `agents.defaults.workspace` 作为 fallback

### 1.0.0
- 初始版本：主动发送飞书文本消息

## 许可证 | License

MIT


path: clawdis.yaml
content: name: feishu-proactive-messenger
version: 1.0.1
description: 飞书主动消息发送器 — 飞书渠道只支持被动回复，agent 无法主动发起对话。本 skill 调用飞书 OpenAPI 发送文本消息，补齐主动投递能力。| Feishu Proactive Messenger — Send text messages proactively via Feishu OpenAPI, filling the channel's missing outbound messaging.
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
    - Reads channels.feishu.accounts.*.defaultTo
security:
  install_time_executes_code: false
  runtime_agent_executes_code: true
  runtime_agent_network_access: true
  reads_local_config: true
tags:
  - feishu
  - message
  - proactive
  - outbound
  - openapi
  - messaging


path: scripts/feishu_proactive_messenger.py
content: #!/usr/bin/env python3
"""
feishu-proactive-messenger
Send a text message to Feishu proactively using the current agent's credentials.
"""

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_SEND_MSG_URL = "https://open.feishu.cn/open-apis/im/v1/messages"
FEISHU_BOT_INFO_URL = "https://open.feishu.cn/open-apis/bot/v3/info"
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()
    default_workspace = (
        config.get("agents", {}).get("defaults", {}).get("workspace")
    )
    best_match = (0, None)
    for agent in config.get("agents", {}).get("list", []):
        workspace = agent.get("workspace") or default_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, Optional[str]]:
    """Returns (app_id, app_secret, default_to)."""
    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}")
    default_to = account.get("defaultTo")
    return app_id, app_secret, default_to


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 get_bot_name(token: str) -> str:
    """Get the bot's display name from Feishu. Returns empty string on failure."""
    try:
        resp = requests.get(
            FEISHU_BOT_INFO_URL,
            headers={"Authorization": f"Bearer {token}"},
            timeout=10,
        )
        resp.raise_for_status()
        data = resp.json()
        if data.get("code") == 0:
            return data.get("bot", {}).get("app_name", "")
    except Exception:
        pass
    return ""


def send_text_message(
    token: str,
    receive_id: str,
    receive_id_type: str,
    text: 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": "text",
        "content": json.dumps({"text": text}),
    }
    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 "open_id"


def resolve_receive_id(cli_value: Optional[str], default_to: Optional[str]) -> str:
    """Resolve receive_id from CLI arg, env, or defaultTo config."""
    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

    if default_to:
        # Strip "user:" prefix if present (OpenClaw internal format)
        if default_to.startswith("user:"):
            return default_to[5:]
        return default_to

    raise RuntimeError(
        "Missing receive_id. Provide --receive-id, set OPENCLAW_CHAT_ID, "
        "or configure defaultTo in the Feishu account."
    )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Send a text message to Feishu proactively"
    )
    parser.add_argument("--text", required=True, help="Message text to send")
    parser.add_argument("--agent", default=None, help="Agent id (e.g. coder, data). Auto-detect from cwd if omitted")
    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)",
    )
    return parser.parse_args()


def main() -> None:
    args = parse_args()

    config = load_openclaw_config()
    agent_id = args.agent or resolve_agent_id(config)
    app_id, app_secret, default_to = resolve_feishu_account(config, agent_id)

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

    [REDACTED_SECRET], app_secret)
    bot_name = get_bot_name(token)
    result = send_text_message(token, receive_id, receive_id_type, args.text)
    label = bot_name if bot_name else agent_id
    print(f"✅ [{label}] 消息已发送")


if __name__ == "__main__":
    main()


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