Skill: ysjyga/webhook-push (version 1.0.3)

--- name: webhook-push description: 多平台群机器人消息推送。支持企业微信、钉钉、飞书的 webhook 推送。当用户说"发送到微信群"、"发钉钉消息"、"推送到飞书"时使用此技能。 metadata: files: - path: webhook-config.json description: 用户 webhook key 配置文件，由用户在安装后自行创建，技能仅读取此文件获取 key required: false user_provided: true --- # 多平台群机器人消息推送 ## 快速开始 ### 首次配置 用户需要先配置各平台的 webhook key。支持的平台： | 平台 | 说明 | |------|------| | 企业微信 | 群机器人 webhook | | 钉钉 | 自定义机器人 | | 飞书 | 自定义机器人 | 将配置保存到 `webhook-config.json`（放在脚本同目录下）： ```json { "wechat": { "群名称": { "key": "YOUR_WECHAT_KEY_HERE" } }, "dingtalk": {}, "feishu": {} } ``` --- ## 支持的平台 ### 1. 企业微信 **API 地址**: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}` **支持的消息类型**: - `text` - 文本消息（支持 @mention） - `image` - 图片（base64 + md5） - `markdown` - Markdown（部分支持） **发送示例**: ```python import json import urllib.request def send_wechat(key, content): url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}" data = {"msgtype": "text", "text": {"content": content}} req = urllib.request.Request(url, data=json.dumps(data, ensure_ascii=False).encode('utf-8')) req.add_header("Content-Type", "application/json; charset=utf-8") with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode('utf-8')) ``` --- ### 2. 钉钉 **API 地址**: `https://oapi.dingtalk.com/robot/send?access_token={key}` **支持的消息类型**: - `text` - 文本消息（支持 @手机号） - `markdown` - Markdown - `link` - 链接卡片 - `actionCard` - ActionCard 卡片 - `feedCard` - FeedCard 卡片 **发送示例**: ```python import json import urllib.request def send_dingtalk(key, content, at_mobiles=None): url = f"https://oapi.dingtalk.com/robot/send?access_token={key}" data = { "msgtype": "text", "text": {"content": content}, "at": {"atMobiles": at_mobiles or []} } req = urllib.request.Request(url, data=json.dumps(data, ensure_ascii=False).encode('utf-8')) req.add_header("Content-Type", "application/json; charset=utf-8") with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode('utf-8')) ``` --- ### 3. 飞书 **API 地址**: `https://open.feishu.cn/open-apis/bot/v2/hook/{key}` **支持的消息类型**: - `text` - 纯文本 - `post` - 富文本 - `image` - 图片（需先上传获取 media_id） - `share_chat` - 分享群会话 - `interactive` - 卡片消息 **发送示例**: ```python import json import urllib.request def send_feishu(key, content): url = f"https://open.feishu.cn/open-apis/bot/v2/hook/{key}" data = {"msg_type": "text", "content": {"text": content}} req = urllib.request.Request(url, data=json.dumps(data, ensure_ascii=False).encode('utf-8')) req.add_header("Content-Type", "application/json; charset=utf-8") with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode('utf-8')) ``` --- ## 使用示例 **用户说**：「把天气发到钉钉项目群」 **执行流程**： 1. 从配置中查找「钉钉-项目群」对应的 key 2. 获取天气数据 3. 调用 send_dingtalk 发送 --- ## @mentioned 功能（企业微信） ### @所有人 ✅ 推荐方式 ```python from webhook_push import send_wechat_text key = "YOUR_WEBHOOK_KEY" content = "明天晚上公司聚餐，大家都来！" # 内容里不要写 @all send_wechat_text(key, content, mentioned_list=["@all"]) ``` **注意**：内容里**不要写 @all**，只需要传 `mentioned_list=["@all"]`。如果内容里写了 @all，消息会显示两个 @all。 ### @个人（临时方案） 企业微信 webhook 的 @ 个人功能需要获取成员的 userid 或手机号。如果无法获取，可用以下方式： ```python # 在内容前面直接写 @姓名（这是临时方案，不会触发系统通知） content = "@张三 明天上午10点开会" send_wechat_text(key, content) ``` **效果**：消息中会显示 "@张三"，但这是纯文本显示，不会触发系统的 @ 通知。 ### @个人（正确方式，需要 userid/手机号） 如果能获取到成员的 userid 或手机号，可以使用官方支持的 @ 方式： ```python # 通过 userid @（userid 需要管理员在企业微信后台获取） send_wechat_text(key, content, mentioned_list=["userid"]) # 通过手机号 @（需要在群里绑定过手机号） send_wechat_text(key, content, mentioned_mobile_list=["13800001111"]) ``` **注意**： - `mentioned_list` 里 userid 前**不要加 @ 符号**，如 `["wangqing"]` 而不是 `["@wangqing"]` - `mentioned_list=["@all"]` 里 @all **需要带 @ 符号** ### 权限要求 群聊设置中需要开启「允许机器人 @ 所有人」权限（需群主/管理员操作）。 --- ## 常见错误 | 平台 | errcode | 解决方案 | |------|---------|----------| | 企业微信 | 301019 | 图片 MD5 不匹配 | | 企业微信 | 93000 | webhook key 无效 | | 钉钉 | 40014 | access_token 无效 | | 钉钉 | 40035 | at_mobiles 格式错误 | | 飞书 | 99991663 | webhook URL 无效 | | 飞书 | 230001 | 消息内容过长 | --- ## 注意事项 1. **频率限制**： - 企业微信：每分钟 20 条 - 钉钉：每分钟 20 条 - 飞书：每分钟 100 条 2. **内容限制**： - 钉钉 Markdown 支持有限标题、列表、链接等 - 飞书卡片支持更丰富的交互 3. **敏感词**：各平台都会对内容进行过滤

path: README.md
content: # webhook-push 多平台群机器人消息推送技能

> 一句话生成，多端推送：把消息发送到企业微信、钉钉、飞书群

---

## 功能简介

本技能支持同时向企业微信、钉钉、飞书三大平台的群机器人发送消息。用户只需一句话，即可触发推送：

- 「把天气预报发到项目群」
- 「通知大家明天开会」
- 「给钉钉群发个测试消息」
- 「推送到飞书」

---

## 快速开始

### 1. 安装技能

将 `webhook-push` 目录放入 skills 安装目录即可（OpenClaw 会自动识别）。

### 2. 配置 Webhook Key

在各平台群聊中添加群机器人，获取 webhook 地址中的 key，保存到配置文件。

#### 企业微信

1. 打开目标群聊 → 点击右上角「...」
2. 进入「群设置」→「添加群机器人」
3. 添加「群机器人」→ 复制 webhook 地址中的 key

   示例地址：
   ```
   https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=XXXX-XXXX-XXXX
   ```
   复制的 key：`XXXX-XXXX-XXXX`

#### 钉钉

1. 打开目标群聊 → 点击右上角「...」
2. 进入「群设置」→「智能群助手」
3. 添加机器人 → 选择「自定义」→ 复制 access_token

#### 飞书

1. 打开目标群聊 → 点击右上角「...」
2. 进入「群设置」→「群机器人」
3. 添加机器人 → 选择「自定义」→ 复制 webhook 地址中的 hook 字段

### 3. 保存配置

将获取的 key 保存到 `webhook-config.json`（放在脚本同目录下）：

```json
{
  "wechat": {
    "群名称": {
      "key": "YOUR_WECHAT_KEY"
    }
  },
  "dingtalk": {
    "群名称": {
      "key": "YOUR_DINGTALK_TOKEN"
    }
  },
  "feishu": {
    "群名称": {
      "key": "YOUR_FEISHU_HOOK"
    }
  }
}
```

> ⚠️ **安全提示**：`webhook-config.json` 文件仅存储在本地，由用户自行保管，请勿上传到 GitHub 或其他公开场所。
```

---

## 使用示例

### 发送文本消息

**用户说**：
> 发个消息到企业微信项目群

**执行**：
```python
from webhook_push import send_wechat_text

key = "YOUR_WECHAT_KEY"
content = "大家好，这是测试消息"
send_wechat_text(key, content)
```

---

### @所有人

**用户说**：
> 通知群里所有人明天上午10点开会

**执行**：
```python
content = "明天上午10点在会议室开会，请大家准时参加！"
send_wechat_text(key, content, mentioned_list=["@all"])
```

**注意**：
- 内容里**不要写 @all**，只需要传 `mentioned_list=["@all"]`
- 如果内容里写了 @all，消息会显示两个 @all

---

### @个人（需要 userid 或手机号）

**用户说**：
> @张三 明天上午10点开会

**执行**：
```python
# 方式一：通过手机号 @（成员需在群里绑定过手机号）
send_wechat_text(key, content, mentioned_mobile_list=["13800001111"])

# 方式二：通过 userid @（需要管理员提供 userid）
send_wechat_text(key, content, mentioned_list=["zhangsan"])
```

**注意**：`mentioned_list` 里 userid 前**不要加 @** 符号，如 `["zhangsan"]` 而不是 `["@zhangsan"]`。

---

### 发送图片

**用户说**：
> 把这张图发到群里

```python
from webhook_push import send_wechat_image

key = "YOUR_WECHAT_KEY"
image_path = "C:/Users/demo/chart.png"
send_wechat_image(key, image_path)
```

---

### 发送 Markdown（企业微信）

```python
from webhook_push import send_wechat_markdown

key = "YOUR_WECHAT_KEY"
content = "**会议提醒**\n\n- 时间：明天上午10点\n- 地点：会议室A\n- 主持人：张三"
send_wechat_markdown(key, content)
```

> ⚠️ 企业微信 Markdown 支持有限，仅支持部分 Markdown 语法。

---

### 钉钉消息

```python
from webhook_push import send_dingtalk_text

key = "YOUR_DINGTALK_TOKEN"
content = "大家好，这是钉钉群的消息"
send_dingtalk_text(key, content, at_mobiles=["13800001111"])
```

---

### 飞书消息

```python
from webhook_push import send_feishu_text

key = "YOUR_FEISHU_HOOK"
content = "大家好，这是飞书群的消息"
send_feishu_text(key, content)
```

---

## 支持的消息类型

### 企业微信

| 消息类型 | 说明 | @功能 |
|----------|------|-------|
| text | 文本消息 | ✅ 支持 |
| image | 图片 | ❌ |
| markdown | Markdown | ❌ |

### 钉钉

| 消息类型 | 说明 | @功能 |
|----------|------|-------|
| text | 文本消息 | ✅ 支持 |
| markdown | Markdown | ❌ |
| link | 链接卡片 | ❌ |
| actionCard | ActionCard 卡片 | ❌ |
| feedCard | FeedCard 卡片 | ❌ |

### 飞书

| 消息类型 | 说明 | @功能 |
|----------|------|-------|
| text | 纯文本 | ❌ |
| post | 富文本 | ❌ |
| image | 图片（需 media_id） | ❌ |
| interactive | 卡片消息 | ❌ |

---

## 常见错误处理

| 平台 | 错误码 | 含义 | 解决方案 |
|------|--------|------|----------|
| 企业微信 | 301019 | 图片 MD5 不匹配 | 重新计算文件 MD5 |
| 企业微信 | 93000 | webhook key 无效 | 检查 key 是否正确 |
| 企业微信 | 30004 | 消息被频率限制 | 降低发送频率 |
| 钉钉 | 40014 | access_token 无效 | 重新获取 token |
| 钉钉 | 40035 | at_mobiles 格式错误 | 检查手机号格式 |
| 钉钉 | 43004 | 机器人功能未开启 | 在群设置中开启 |
| 飞书 | 99991663 | webhook URL 无效 | 检查 hook 是否正确 |
| 飞书 | 230001 | 消息内容过长 | 缩短内容或拆分发送 |

---

## 频率限制

| 平台 | 限制 |
|------|------|
| 企业微信 | 每分钟 20 条 |
| 钉钉 | 每分钟 20 条 |
| 飞书 | 每分钟 100 条 |

---

## 文件结构

```
webhook-push/
├── SKILL.md              # 技能说明（供 AI 阅读）
├── README.md             # 使用文档（供用户阅读）
├── scripts/
│   └── webhook_push.py   # 推送脚本
├── references/           # 参考资料
│   ├── wechat_api.md     # 企业微信 API 文档
│   ├── dingtalk_api.md   # 钉钉 API 文档
│   └── feishu_api.md     # 飞书 API 文档
└── assets/              # 资源文件
```

---

## 更新日志

### v1.0.0
- 支持企业微信、钉钉、飞书三平台
- 支持文本、图片、Markdown 消息
- 支持 @mention 功能
- 配置文件独立管理

---

## License

MIT License


path: scripts/webhook_push.py
content: # -*- coding: utf-8 -*-
"""
多平台群机器人消息推送
支持：企业微信、钉钉、飞书
配置文件：webhook-config.json
"""
import json
import urllib.request
import base64
import hashlib
import os
import pathlib

# 配置文件路径 - 当前目录下的 webhook-config.json
CONFIG_PATH = pathlib.Path("webhook-config.json")


def load_config():
    """加载配置文件"""
    if not os.path.exists(CONFIG_PATH):
        raise FileNotFoundError(
            f"[配置错误] 配置文件不存在: {CONFIG_PATH}\n"
            "请先创建 webhook-config.json 文件，参考 SKILL.md 中的配置格式。"
        )
    
    with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
        return json.load(f)


def get_key(platform, group_name):
    """根据平台和群名称获取 webhook key"""
    try:
        config = load_config()
    except FileNotFoundError:
        raise  # 直接抛出，让调用方统一处理
    
    platform_lower = platform.lower()
    if platform_lower not in ["wechat", "dingtalk", "feishu"]:
        raise ValueError(f"[参数错误] 不支持的平台: {platform}，仅支持 wechat / dingtalk / feishu")
    
    platform_config = config.get(platform_lower, {})
    group_config = platform_config.get(group_name, {})
    key = group_config.get("key")
    
    if not key:
        raise KeyError(
            f"[配置错误] 未在 webhook-config.json 中找到群 '{group_name}' 的 key。\n"
            f"请在 {platform} 下添加 '{group_name}' 的配置，参考：\n"
            f'  "{group_name}": {{"key": "你的webhook key"}}'
        )
    
    placeholder_keys = ["YOUR_WECHAT_KEY_HERE", "YOUR_DINGTALK_KEY_HERE", "YOUR_FEISHU_KEY_HERE"]
    if key in placeholder_keys:
        raise ValueError(
            f"[配置错误] 群 '{group_name}' 的 key 仍是占位符，请替换为真实的 webhook key。"
        )
    
    return key


# ========== 企业微信 ==========

def send_wechat_text(key, content, mentioned_list=None, mentioned_mobile_list=None):
    """发送企业微信文本消息
    
    Args:
        key: webhook key
        content: 消息内容
        mentioned_list: 被 @ 的用户 userid 列表，如 ["userid1", "userid2"]
        mentioned_mobile_list: 被 @ 的用户手机号列表，如 ["13800001111"]
    
    注意：要 @ 某人，需要在 mentioned_list 或 mentioned_mobile_list 中指定，
    而不是简单地在内容中写 @名字
    """
    url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}"
    text_data = {"content": content}
    
    if mentioned_list:
        text_data["mentioned_list"] = mentioned_list
    if mentioned_mobile_list:
        text_data["mentioned_mobile_list"] = mentioned_mobile_list
    
    data = {
        "msgtype": "text",
        "text": text_data
    }
    
    req = urllib.request.Request(url, data=json.dumps(data, ensure_ascii=False).encode('utf-8'))
    req.add_header("Content-Type", "application/json; charset=utf-8")
    
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.loads(resp.read().decode('utf-8'))


def send_wechat_image(key, image_path):
    """发送企业微信图片消息（base64 + md5 方式）"""
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"图片文件不存在: {image_path}")
    
    with open(image_path, 'rb') as f:
        data = f.read()
        md5 = hashlib.md5(data).hexdigest()
        b64 = base64.b64encode(data).decode('utf-8')
    
    url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}"
    msg = {
        "msgtype": "image",
        "image": {"base64": b64, "md5": md5}
    }
    
    req = urllib.request.Request(url, data=json.dumps(msg).encode('utf-8'))
    req.add_header("Content-Type", "application/json; charset=utf-8")
    
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode('utf-8'))


def send_wechat(group_name, content, at_mobiles=None):
    """发送企业微信消息（从配置文件读取 key）
    
    Args:
        group_name: 群名称（在 webhook-config.json 中配置的名称）
        content: 消息内容
        at_mobiles: 需要 @ 的手机号列表，如 ["13800001111"]
                   这些人的手机号会在消息中显示被 @
    
    示例：
        send_wechat("OpenClawTest", "会议通知", at_mobiles=["13800001111"])
    """
    key = get_key("wechat", group_name)
    
    mentioned_mobile_list = at_mobiles if at_mobiles else None
    
    # 如果内容中有 @xxx 格式但没有提供 at_mobiles，
    # 企业微信不会触发 @ 效果，需要通过 mentioned_mobile_list 来实现
    return send_wechat_text(key, content, mentioned_mobile_list=mentioned_mobile_list)


def send_wechat_image_by_name(group_name, image_path):
    """发送企业微信图片（从配置文件读取 key）"""
    key = get_key("wechat", group_name)
    return send_wechat_image(key, image_path)


# ========== 钉钉 ==========

def send_dingtalk_text(key, content, at_mobiles=None):
    """发送钉钉文本消息"""
    url = f"https://oapi.dingtalk.com/robot/send?access_token={key}"
    data = {
        "msgtype": "text",
        "text": {"content": content},
        "at": {"atMobiles": at_mobiles or []}
    }
    
    req = urllib.request.Request(url, data=json.dumps(data, ensure_ascii=False).encode('utf-8'))
    req.add_header("Content-Type", "application/json; charset=utf-8")
    
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.loads(resp.read().decode('utf-8'))


def send_dingtalk_markdown(key, title, content):
    """发送钉钉 Markdown 消息"""
    url = f"https://oapi.dingtalk.com/robot/send?access_token={key}"
    data = {
        "msgtype": "markdown",
        "markdown": {"title": title, "text": content}
    }
    
    req = urllib.request.Request(url, data=json.dumps(data, ensure_ascii=False).encode('utf-8'))
    req.add_header("Content-Type", "application/json; charset=utf-8")
    
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.loads(resp.read().decode('utf-8'))


def send_dingtalk_link(key, title, text, message_url, pic_url=""):
    """发送钉钉链接卡片"""
    url = f"https://oapi.dingtalk.com/robot/send?access_token={key}"
    data = {
        "msgtype": "link",
        "link": {
            "title": title,
            "text": text,
            "messageUrl": message_url,
            "picUrl": pic_url
        }
    }
    
    req = urllib.request.Request(url, data=json.dumps(data, ensure_ascii=False).encode('utf-8'))
    req.add_header("Content-Type", "application/json; charset=utf-8")
    
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.loads(resp.read().decode('utf-8'))


def send_dingtalk(group_name, content, at_mobiles=None):
    """发送钉钉消息（从配置文件读取 key）"""
    key = get_key("dingtalk", group_name)
    return send_dingtalk_text(key, content, at_mobiles)


# ========== 飞书 ==========

def send_feishu_text(key, content):
    """发送飞书文本消息"""
    url = f"https://open.feishu.cn/open-apis/bot/v2/hook/{key}"
    data = {
        "msg_type": "text",
        "content": {"text": content}
    }
    
    req = urllib.request.Request(url, data=json.dumps(data, ensure_ascii=False).encode('utf-8'))
    req.add_header("Content-Type", "application/json; charset=utf-8")
    
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.loads(resp.read().decode('utf-8'))


def send_feishu_post(key, title, content):
    """发送飞书富文本消息"""
    url = f"https://open.feishu.cn/open-apis/bot/v2/hook/{key}"
    data = {
        "msg_type": "post",
        "content": {
            "post": {
                "zh_cn": {
                    "title": title,
                    "content": [
                        [{"tag": "text", "text": content}]
                    ]
                }
            }
        }
    }
    
    req = urllib.request.Request(url, data=json.dumps(data, ensure_ascii=False).encode('utf-8'))
    req.add_header("Content-Type", "application/json; charset=utf-8")
    
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.loads(resp.read().decode('utf-8'))


def send_feishu(group_name, content):
    """发送飞书消息（从配置文件读取 key）"""
    key = get_key("feishu", group_name)
    return send_feishu_text(key, content)


# ========== 主函数 ==========

if __name__ == "__main__":
    import sys
    
    if len(sys.argv) < 4:
        print("=== 多平台群消息推送 ===")
        print()
        print("用法:")
        print("  企业微信: python webhook_push.py wechat <群名称> <消息>")
        print("  钉钉:     python webhook_push.py dingtalk <群名称> <消息>")
        print("  飞书:     python webhook_push.py feishu <群名称> <消息>")
        print()
        print("示例:")
        print("  python webhook_push.py wechat OpenClawTest 测试消息")
        print("  python webhook_push.py dingtalk 项目群 测试消息")
        print("  python webhook_push.py feishu 项目群 测试消息")
        print()
        print(f"配置文件: {CONFIG_PATH}")
        print()
        print("常见错误:")
        print("  - 配置文件不存在 → 创建 webhook-config.json")
        print("  - 未找到群的key → 在配置文件中添加群配置")
        print("  - key是占位符 → 替换为真实webhook key")
        sys.exit(1)
    
    platform = sys.argv[1].lower()
    group_name = sys.argv[2]
    message = " ".join(sys.argv[3:])
    
    try:
        if platform == "wechat":
            result = send_wechat(group_name, message)
        elif platform == "dingtalk":
            result = send_dingtalk(group_name, message)
        elif platform == "feishu":
            result = send_feishu(group_name, message)
        else:
            print(f"[错误] 不支持的平台: {platform}")
            sys.exit(1)
        
        if result.get("errcode") == 0:
            print(f"[成功] 消息已发送到 {group_name}")
        else:
            print(f"[失败] 企业微信返回错误: {result}")
            sys.exit(1)
    except (KeyError, FileNotFoundError, ValueError) as e:
        print(f"\n[错误] {e}")
        sys.exit(1)

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