Skill: kururu111/feishu-send-file-1-2-1 (version 1.0.0)

--- name: feishu-send-file description: 通过飞书机器人稳定发送本地普通文件或本地图片。用于现有一等工具无法直接完成“发送本地文件附件”时，或本地图片经常规消息链路发送后在飞书里只显示路径文本而不显示图片本体时。普通文件走 `im/v1/files -> file_key -> msg_type=file`，图片走 `im/v1/images -> image_key -> msg_type=image`。 --- # 飞书发送文件 使用这个 skill 发送**本地普通文件**，或补救“**本地图片被错误发成路径文本**”的场景。 优先使用平台已有的一等飞书工具。只有在以下情况出现时，再使用本 skill： - 需要以机器人身份发送本地普通文件附件 - 现有工具只能把本地路径当作文本发出去 - 本地图片走常规消息链路后，飞书里显示的是路径文本而不是图片本体 ## 工作方式 ### 普通文件 飞书普通文件消息需要两步： 1. 上传文件到 `im/v1/files` 2. 使用返回的 `file_key` 发送 `msg_type=file` ### 图片 飞书图片消息也需要两步： 1. 上传图片到 `im/v1/images` 2. 使用返回的 `image_key` 发送 `msg_type=image` 不要混用这两条链路。 ## 何时切换到本 skill 如果本地图片通过常规消息路径发送后，用户在飞书里看到的是： - 本地绝对路径，例如 `/path/to/demo.png` - 带附件图标的路径文本 - 任何不是图片本体的回显 就视为这次发送失败，不要继续重试同一种方式，直接改用 `scripts/send_image.py`。 **成功标准只有一个：用户在飞书里实际看到文件附件或图片本体。** ## 脚本 ### 发送普通文件 ```bash python3 scripts/send_file.py <file_path> <receive_id_type> <receive_id> <app_id> <app_secret> [file_name] [domain] ``` 参数： - `file_path`: 本地文件路径 - `receive_id_type`: `open_id` 或 `chat_id` - `receive_id`: 接收者 ID - `app_id`: 飞书或 Lark 应用 ID - `app_secret`: 飞书或 Lark 应用密钥 - `file_name`: 可选，自定义显示文件名 - `domain`: 可选，`feishu` 或 `lark`，默认 `feishu` 示例： ```bash python3 scripts/send_file.py /path/to/report.html open_id ou_xxx cli_xxx secret_xxx python3 scripts/send_file.py /path/to/archive.zip chat_id oc_xxx cli_xxx secret_xxx backup.zip python3 scripts/send_file.py /path/to/report.pdf open_id ou_xxx cli_xxx secret_xxx report.pdf lark ``` ### 稳定发送图片 ```bash python3 scripts/send_image.py <image_path> <receive_id_type> <receive_id> <app_id> <app_secret> [domain] ``` 参数： - `image_path`: 本地图片路径 - `receive_id_type`: `open_id` 或 `chat_id` - `receive_id`: 接收者 ID - `app_id`: 飞书或 Lark 应用 ID - `app_secret`: 飞书或 Lark 应用密钥 - `domain`: 可选，`feishu` 或 `lark`，默认 `feishu` 示例： ```bash python3 scripts/send_image.py /path/to/demo.png open_id ou_xxx cli_xxx secret_xxx python3 scripts/send_image.py /path/to/demo.png chat_id oc_xxx cli_xxx secret_xxx lark ``` ## 获取参数 - `app_id` / `app_secret` 来自当前飞书或 Lark 应用配置 - `receive_id_type` 取决于目标是私聊还是群聊 - `receive_id` 需要从当前消息上下文、用户映射、会话元数据或上游系统明确取得 - 如果上游上下文给的是 `user:ou_xxx` 之类的复合格式，先提取真正的 ID 再传给脚本 不要把某个环境里的固定路径、固定配置文件位置、固定 inbound 字段格式写死为通用规则。 ## 手动 API 流程 ### 普通文件 1. 获取 tenant access token 2. 调用 `im/v1/files` 上传文件 3. 使用返回的 `file_key` 发送 `msg_type=file` ### 图片 1. 获取 tenant access token 2. 调用 `im/v1/images` 上传图片 3. 使用返回的 `image_key` 发送 `msg_type=image` ## 注意事项 - 普通文件和图片必须走各自独立的上传接口 - 如果常规消息链路返回了 `message_id`，也不能直接判定成功，仍要看飞书客户端最终显示结果 - 本 skill 面向“本地文件发送”这一缺口，不替代平台现有的一等飞书工具 - 需要群发或群聊发送时，使用 `chat_id` - 需要国际版 Lark 时，将最后一个参数设为 `lark`

path: _meta.json
content: ownerId: kn7a841362aj7bgq4pvncj9szx80z084
slug: feishu-send-file
version: 1.2.1
publishedAt: 1773558978040

path: scripts/send_file.py
content: #!/usr/bin/env python3
"""
飞书/Lark 发送普通文件脚本

用法:
  python3 send_file.py <file_path> <receive_id_type> <receive_id> <app_id> <app_secret> [file_name] [domain]

示例:
  python3 send_file.py /path/to/report.html open_id ou_xxx cli_xxx secret_xxx
  python3 send_file.py /path/to/archive.zip chat_id oc_xxx cli_xxx secret_xxx backup.zip
  python3 send_file.py /path/to/report.pdf open_id ou_xxx cli_xxx secret_xxx report.pdf lark
"""

import json
import os
import subprocess
import sys
import urllib.error
import urllib.request


VALID_RECEIVE_ID_TYPES = {"open_id", "chat_id"}
VALID_DOMAINS = {
    "feishu": "https://open.feishu.cn",
    "lark": "https://open.larksuite.com",
}


def get_base(domain: str) -> str:
    if domain not in VALID_DOMAINS:
        raise ValueError(f"不支持的 domain: {domain}，仅支持: {', '.join(sorted(VALID_DOMAINS))}")
    return VALID_DOMAINS[domain]


def decode_json_bytes(raw: bytes, context: str) -> dict:
    try:
        text = raw.decode("utf-8")
    except UnicodeDecodeError as exc:
        raise RuntimeError(f"{context} 返回了非 UTF-8 内容") from exc

    try:
        return json.loads(text)
    except json.JSONDecodeError as exc:
        raise RuntimeError(f"{context} 返回了非 JSON 内容: {text[:500]}") from exc


def get_tenant_token(base: str, app_id: str, app_secret: str) -> str:
    url = f"{base}/open-apis/auth/v3/tenant_access_token/internal"
    payload = json.dumps({"app_id": app_id, "app_secret": app_secret}).encode()
    req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})

    try:
        with urllib.request.urlopen(req) as resp:
            result = decode_json_bytes(resp.read(), "获取 token")
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"获取 token HTTP 失败: {exc.code} {body}") from exc

    if result.get("code") != 0:
        raise RuntimeError(f"获取 token 失败: {result}")

    [REDACTED_SECRET]"tenant_access_token")
    if not token:
        raise RuntimeError(f"获取 token 失败，返回中缺少 tenant_access_token: {result}")
    return token


def upload_file(base: str, token: str, file_path: str, file_name: str) -> str:
    result = subprocess.run(
        [
            "curl",
            "-sS",
            "-X",
            "POST",
            f"{base}/open-apis/im/v1/files",
            "-H",
            f"Authorization: Bearer {token}",
            "-F",
            "file_type=stream",
            "-F",
            f"file_name={file_name}",
            "-F",
            f"file=@{file_path}",
        ],
        capture_output=True,
        text=True,
    )

    if result.returncode != 0:
        raise RuntimeError(f"上传文件失败: {result.stderr.strip() or 'curl 执行失败'}")

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError as exc:
        raise RuntimeError(f"上传文件返回了非 JSON 内容: {result.stdout[:500]}") from exc

    if data.get("code") != 0:
        raise RuntimeError(f"上传文件失败: {data}")

    file_key = data.get("data", {}).get("file_key")
    if not file_key:
        raise RuntimeError(f"上传文件失败，未返回 file_key: {data}")

    return file_key


def send_file_message(base: str, token: str, receive_id_type: str, receive_id: str, file_key: str) -> dict:
    url = f"{base}/open-apis/im/v1/messages?receive_id_type={receive_id_type}"
    payload = {
        "receive_id": receive_id,
        "msg_type": "file",
        "content": json.dumps({"file_key": file_key}, ensure_ascii=False),
    }
    data = json.dumps(payload, ensure_ascii=False).encode()
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        },
    )

    try:
        with urllib.request.urlopen(req) as resp:
            result = decode_json_bytes(resp.read(), "发送文件消息")
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"发送文件消息 HTTP 失败: {exc.code} {body}") from exc

    if result.get("code") != 0:
        raise RuntimeError(f"发送文件消息失败: {result}")

    return result


def main() -> None:
    if len(sys.argv) < 6:
        print("用法: python3 send_file.py <file_path> <receive_id_type> <receive_id> <app_id> <app_secret> [file_name] [domain]")
        sys.exit(1)

    file_path = sys.argv[1]
    receive_id_type = sys.argv[2]
    receive_id = sys.argv[3]
    app_id = sys.argv[4]
    app_secret = sys.argv[5]
    file_name = sys.argv[6] if len(sys.argv) > 6 else os.path.basename(file_path)
    domain = sys.argv[7] if len(sys.argv) > 7 else "feishu"

    if receive_id_type not in VALID_RECEIVE_ID_TYPES:
        print(f"ERROR: receive_id_type 仅支持 {', '.join(sorted(VALID_RECEIVE_ID_TYPES))}")
        sys.exit(1)

    if not os.path.isfile(file_path):
        print(f"ERROR: 文件不存在: {file_path}")
        sys.exit(1)

    try:
        base = get_base(domain)
        print(f"📎 发送文件: {file_name}")
        print(f"🎯 接收类型: {receive_id_type}")
        print(f"🌐 Domain: {domain} ({base})")

        [REDACTED_SECRET], app_id, app_secret)
        print("✅ 获取 token 成功")

        file_key = upload_file(base, token, file_path, file_name)
        print(f"✅ 上传成功, file_key: {file_key}")

        result = send_file_message(base, token, receive_id_type, receive_id, file_key)
        message_id = result.get("data", {}).get("message_id")
        print(f"✅ 发送成功, message_id: {message_id}")
    except Exception as exc:
        print(f"ERROR: {exc}")
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/send_image.py
content: #!/usr/bin/env python3
"""
飞书/Lark 稳定发送图片脚本

用途:
- 当常规消息链路把本地图片错误发送为路径文本时，直接走官方上传图片接口

用法:
  python3 send_image.py <image_path> <receive_id_type> <receive_id> <app_id> <app_secret> [domain]

示例:
  python3 send_image.py /path/to/demo.png open_id ou_xxx cli_xxx secret_xxx
  python3 send_image.py /path/to/demo.png chat_id oc_xxx cli_xxx secret_xxx lark
"""

import json
import os
import subprocess
import sys
import urllib.error
import urllib.request


VALID_RECEIVE_ID_TYPES = {"open_id", "chat_id"}
VALID_DOMAINS = {
    "feishu": "https://open.feishu.cn",
    "lark": "https://open.larksuite.com",
}


def get_base(domain: str) -> str:
    if domain not in VALID_DOMAINS:
        raise ValueError(f"不支持的 domain: {domain}，仅支持: {', '.join(sorted(VALID_DOMAINS))}")
    return VALID_DOMAINS[domain]


def decode_json_bytes(raw: bytes, context: str) -> dict:
    try:
        text = raw.decode("utf-8")
    except UnicodeDecodeError as exc:
        raise RuntimeError(f"{context} 返回了非 UTF-8 内容") from exc

    try:
        return json.loads(text)
    except json.JSONDecodeError as exc:
        raise RuntimeError(f"{context} 返回了非 JSON 内容: {text[:500]}") from exc


def get_tenant_token(base: str, app_id: str, app_secret: str) -> str:
    url = f"{base}/open-apis/auth/v3/tenant_access_token/internal"
    payload = json.dumps({"app_id": app_id, "app_secret": app_secret}).encode()
    req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})

    try:
        with urllib.request.urlopen(req) as resp:
            result = decode_json_bytes(resp.read(), "获取 token")
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"获取 token HTTP 失败: {exc.code} {body}") from exc

    if result.get("code") != 0:
        raise RuntimeError(f"获取 token 失败: {result}")

    [REDACTED_SECRET]"tenant_access_token")
    if not token:
        raise RuntimeError(f"获取 token 失败，返回中缺少 tenant_access_token: {result}")
    return token


def upload_image(base: str, token: str, image_path: str) -> str:
    result = subprocess.run(
        [
            "curl",
            "-sS",
            "-X",
            "POST",
            f"{base}/open-apis/im/v1/images",
            "-H",
            f"Authorization: Bearer {token}",
            "-F",
            "image_type=message",
            "-F",
            f"image=@{image_path}",
        ],
        capture_output=True,
        text=True,
    )

    if result.returncode != 0:
        raise RuntimeError(f"上传图片失败: {result.stderr.strip() or 'curl 执行失败'}")

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError as exc:
        raise RuntimeError(f"上传图片返回了非 JSON 内容: {result.stdout[:500]}") from exc

    if data.get("code") != 0:
        raise RuntimeError(f"上传图片失败: {data}")

    image_key = data.get("data", {}).get("image_key")
    if not image_key:
        raise RuntimeError(f"上传图片失败，未返回 image_key: {data}")

    return image_key


def send_image_message(base: str, token: str, receive_id_type: str, receive_id: str, image_key: str) -> dict:
    url = f"{base}/open-apis/im/v1/messages?receive_id_type={receive_id_type}"
    payload = {
        "receive_id": receive_id,
        "msg_type": "image",
        "content": json.dumps({"image_key": image_key}, ensure_ascii=False),
    }
    data = json.dumps(payload, ensure_ascii=False).encode()
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        },
    )

    try:
        with urllib.request.urlopen(req) as resp:
            result = decode_json_bytes(resp.read(), "发送图片消息")
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"发送图片消息 HTTP 失败: {exc.code} {body}") from exc

    if result.get("code") != 0:
        raise RuntimeError(f"发送图片消息失败: {result}")

    return result


def main() -> None:
    if len(sys.argv) < 6:
        print("用法: python3 send_image.py <image_path> <receive_id_type> <receive_id> <app_id> <app_secret> [domain]")
        sys.exit(1)

    image_path = sys.argv[1]
    receive_id_type = sys.argv[2]
    receive_id = sys.argv[3]
    app_id = sys.argv[4]
    app_secret = sys.argv[5]
    domain = sys.argv[6] if len(sys.argv) > 6 else "feishu"

    if receive_id_type not in VALID_RECEIVE_ID_TYPES:
        print(f"ERROR: receive_id_type 仅支持 {', '.join(sorted(VALID_RECEIVE_ID_TYPES))}")
        sys.exit(1)

    if not os.path.isfile(image_path):
        print(f"ERROR: 图片不存在: {image_path}")
        sys.exit(1)

    try:
        base = get_base(domain)
        print(f"🖼️ 发送图片: {os.path.basename(image_path)}")
        print(f"🎯 接收类型: {receive_id_type}")
        print(f"🌐 Domain: {domain} ({base})")

        [REDACTED_SECRET], app_id, app_secret)
        print("✅ 获取 token 成功")

        image_key = upload_image(base, token, image_path)
        print(f"✅ 上传成功, image_key: {image_key}")

        result = send_image_message(base, token, receive_id_type, receive_id, image_key)
        message_id = result.get("data", {}).get("message_id")
        print(f"✅ 发送成功, message_id: {message_id}")
    except Exception as exc:
        print(f"ERROR: {exc}")
        sys.exit(1)


if __name__ == "__main__":
    main()


path: skill.json
content: name: feishu-send-file
version: 1.2.1
description: 飞书发送文件技能。用于通过飞书发送普通文件附件（HTML、ZIP、PDF、代码文件等），并处理本地图片路径被错误发送成路径文本的补救场景。普通文件走 `im/v1/files` -> `file_key`；当本地图片经 `message/media` 发送后只显示 `/root/...png` 路径时，改走 `im/v1/images` -> `image_key` 的稳定图片发送脚本。
author: dadaniya99
license: MIT

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