#!/usr/bin/env python3 """ feishu_download.py — 从飞书消息中下载文件附件 用法: python3 feishu_download.py [output_dir] 通过飞书 Open API 获取消息中的 file_key 并下载文件。 自动从 openclaw.json 读取 App ID / Secret 获取 token。 """ import json, sys, os, requests, re CONFIG_PATH = "/root/.openclaw/openclaw.json" FEISHU_API = "https://open.feishu.cn" def get_feishu_creds(): """从 openclaw 配置中提取飞书 App ID / Secret""" if not os.path.exists(CONFIG_PATH): print(f"ERROR: 找不到 {CONFIG_PATH}", file=sys.stderr) sys.exit(1) with open(CONFIG_PATH) as f: cfg = json.load(f) accounts = cfg.get("plugins", {}).get("entries", {}).get("feishu", {}).get("config", {}).get("accounts", {}) default = accounts.get("default", {}) app_id = default.get("appId", "") app_secret = default.get("appSecret", "") if not app_id or not app_secret: print("ERROR: 无法获取飞书 App ID / Secret", file=sys.stderr) sys.exit(1) return app_id, app_secret def get_token(app_id, app_secret): """获取 tenant_access_token""" resp = requests.post(f"{FEISHU_API}/open-apis/auth/v3/tenant_access_token/internal", json={"app_id": app_id, "app_secret": app_secret}) data = resp.json() if data.get("code") != 0: print(f"ERROR: 获取 token 失败: {data}", file=sys.stderr) sys.exit(1) return data["tenant_access_token"] def get_msg_body(message_id, token): """获取消息内容,提取 file_key 和文件名""" resp = requests.get( f"{FEISHU_API}/open-apis/im/v1/messages/{message_id}", headers={"Authorization": f"Bearer {token}"} ) data = resp.json() if data.get("code") != 0: print(f"ERROR: 获取消息失败: {data}", file=sys.stderr) sys.exit(1) items = data.get("data", {}).get("items", []) if not items: print("ERROR: 消息为空", file=sys.stderr) sys.exit(1) body = items[0].get("body", {}) content = json.loads(body.get("content", "{}")) msg_type = body.get("msg_type", "") file_key = content.get("file_key", "") file_name = content.get("file_name", f"file_{message_id}") if not file_key: print(f"ERROR: 消息类型 {msg_type},没有 file_key", file=sys.stderr) print(f" 内容: {content}", file=sys.stderr) sys.exit(1) return file_key, file_name, msg_type def download_file(message_id, file_key, token, output_path): """下载文件到指定路径""" url = f"{FEISHU_API}/open-apis/im/v1/messages/{message_id}/resources/{file_key}?type=file" resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, stream=True) if resp.status_code != 200: print(f"ERROR: 下载失败 (HTTP {resp.status_code}): {resp.text[:200]}", file=sys.stderr) sys.exit(1) with open(output_path, "wb") as f: for chunk in resp.iter_content(chunk_size=65536): if chunk: f.write(chunk) size = os.path.getsize(output_path) return size def safe_filename(name): """清理文件名中的特殊字符""" name = name.strip() # 替换 Windows 不安全的字符 name = re.sub(r'[<>:"/\\|?*]', '_', name) # 压缩多个空格/下划线 name = re.sub(r'[_\s]+', '_', name) return name def main(): if len(sys.argv) < 2: print("用法: python3 feishu_download.py [output_dir]", file=sys.stderr) print("示例: python3 feishu_download.py om_x100b6c16dd10b0acc243c1fa48b0695 /root/projects/uploads", file=sys.stderr) sys.exit(1) message_id = sys.argv[1] output_dir = sys.argv[2] if len(sys.argv) > 2 else "/root/projects/uploads" os.makedirs(output_dir, exist_ok=True) print(f"[1/4] 获取飞书凭证...") app_id, app_secret = get_feishu_creds() print(f"[2/4] 获取 token...") token = get_token(app_id, app_secret) print(f"[3/4] 获取消息 {message_id} 内容...") file_key, file_name, msg_type = get_msg_body(message_id, token) print(f" → file_key: {file_key}") print(f" → file_name: {file_name}") print(f" → msg_type: {msg_type}") safe_name = safe_filename(file_name) output_path = os.path.join(output_dir, safe_name) print(f"[4/4] 下载文件到 {output_path}...") size = download_file(message_id, file_key, token, output_path) print(f" ✅ 下载完成! 大小: {size/1024/1024:.1f} MB") print(f" 📁 {output_path}") # 输出结果信息(供调用方解析) result = { "status": "ok", "message_id": message_id, "file_key": file_key, "file_name": safe_name, "file_path": output_path, "file_size": size } print(f"\nJSON_OUTPUT:{json.dumps(result)}") if __name__ == "__main__": main()