1. OpenClaw 飞书发文件到底难在哪message API 与 Node.js 落地场景拆解OpenClaw 通过飞书发送文件本质上是把「本地文件」变成「飞书会话里可点击下载的附件」。这件事听起来简单但真正动手时会发现它横跨了三层OpenClaw 的 message 工具层、飞书开放平台的鉴权与资源上传接口层、以及你自己用 Node.js 写的业务胶水层。很多人卡住不是因为不会写代码而是不清楚 file_key 从哪来、鉴权走哪条通道、endpoint 该指向谁。这篇内容面向三类人一是已经在用 OpenClaw 做自动化、想把日报/日志/数据包推到飞书的开发者二是想用 Node.js 直接调飞书 message API 发文件、但被 tenant_access_token 和 uploadFile 绕晕的后端同学三是希望把模型调用和文件推送统一到一套 Key/API 通道、不想在多个平台之间反复切换配置的团队。核心检索词就是 OpenClaw 飞书发送文件、message API、Node.js 落地。先说清楚一个容易混淆的点飞书发文件不是「把文件塞进消息体」就完事。飞书的消息接口对文件类型有专门处理你需要先调用资源上传接口拿到一个 file_key再用这个 file_key 去创建消息。这个两步走的设计和很多即时通讯平台一致好处是文件只上传一次、可以被多条消息复用坏处是新手容易在第一步就失败——要么权限没开要么上传的 form-data 字段名写错。OpenClaw 的价值在于它把这套流程封装进了 message 工具。你写一条命令它内部自动完成「检测文件类型 → 调 uploadFile → 拿 file_key → 调 im.message.create」。但封装不等于黑盒一旦报错你还是得回到飞书 API 的原始语义去排查。所以本文不会只给你一条命令就结束而是把 Node.js 侧的原始调用也摊开让你既能用 OpenClaw 快速跑通也能在需要精细控制时自己接管。我试过在同一个项目里混用两种方式日常推送用 OpenClaw 命令行复杂的多文件打包和条件判断用 Node.js 脚本。两者共享同一套飞书应用凭证互不冲突。下面从环境准备开始一步步把链路搭起来。2. TaoToken 统一 Key 前置配置把 endpoint 与鉴权收敛到一条通道在写飞书代码之前先把模型调用和 API 通道的事情理清楚。很多人的项目里飞书应用凭证是一套、模型 API Key 又是另一套散落在不同的 .env 文件里时间一长自己都记不清哪个 Key 对应哪个服务。TaoToken 在这里的作用是提供一个统一的 API 入口让你把模型对话、编码计划、控制台管理这些能力收敛到同一个 Base URL 和同一套 Key 体系下。TaoToken 的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。注意这两个地址的用途不同官网用于注册、查看文档、管理 KeyAPI 基址用于代码里的 endpoint 配置。你需要在官网注册后进入控制台创建 API Key这个 Key 就是后续所有请求的凭证。具体操作路径打开官网登录后进入控制台页面deep link 为 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 管理页 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 创建一个新的 Key。创建时建议按用途命名比如openclaw-feishu-bot方便后续排查。Key 只显示一次复制后立刻存进你的密钥管理工具或本地 .env。如果你用的是 Claude Code 这类编码工具TaoToken 也提供了对应的接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面会说明 Base URL 和 Model ID 怎么填。对于 OpenClaw 场景你主要关心的是当 OpenClaw 需要调用模型来生成文件内容或处理文本时它的模型请求应该指向 TaoToken 的 API 基址而不是散落到各个厂商的原生地址。这里给一个 Node.js 项目里常见的 .env 配置片段把飞书凭证和 TaoToken Key 放在一起管理# .env FEISHU_APP_IDcli_xxxxxxxxxxxx FEISHU_APP_SECRETxxxxxxxxxxxxxxxxxxxxxxxx FEISHU_BOT_TARGETou_xxxxxxxxxxxxxxxxxxxxxxxx TAOTOKEN_API_BASEhttps://taotoken.net/api TAOTOKEN_API_KEYsk-xxxxxxxxxxxxxxxxxxxxxxxx TAOTOKEN_MODEL_IDclaude-3-5-sonnet注意TAOTOKEN_API_BASE结尾不要带斜杠很多 SDK 会自己拼接路径多一个斜杠会导致 404。TAOTOKEN_MODEL_ID按你实际开通的模型填写接入文档里有完整列表。飞书的FEISHU_BOT_TARGET是接收方的 open_id 或 chat_id个人用户以ou_开头群组以oc_开头。把这两套凭证放在同一个 .env 里好处是启动脚本时一次性加载不用在多个文件之间跳。坏处是 .env 绝对不能提交到 Git记得在 .gitignore 里加上。生产环境建议用环境变量注入或密钥管理服务而不是明文文件。配置完成后你可以先用一个最小的 Node.js 脚本验证 TaoToken 通道是否通// check-taotoken.js import fetch from node-fetch; const base process.env.TAOTOKEN_API_BASE; const key process.env.TAOTOKEN_API_KEY; const res await fetch(${base}/v1/models, { headers: { Authorization: Bearer ${key} } }); console.log(res.status, await res.text());如果返回 200 和模型列表说明 Key 和 Base URL 都对。如果返回 401先检查 Key 是否复制完整、有没有多余空格。这一步通了再往下做飞书文件发送排障时就能快速区分是模型通道的问题还是飞书通道的问题。3. 可复制配置飞书应用权限清单与 Node.js 发送脚本飞书发文件失败十有八九是权限没配对。飞书开放平台的应用权限分为「应用权限」和「数据权限」发文件至少需要两个im:resource上传和读取文件资源和im:message:send_as_bot以机器人身份发消息。有些教程只提了后者结果上传文件时返回权限不足排查半天。在飞书开放平台找到你的应用进入「权限管理」搜索并开通这两个权限。开通后需要发布版本权限才会生效。如果你用的是测试企业可以直接在测试版里验证。权限配置的 JSON 结构大致如下方便你对照检查{ scopes: { tenant: [ im:resource, im:message:send_as_bot ] } }tenant表示应用维度的权限适用于机器人主动发消息的场景。如果你的应用还需要读取用户发的文件那要额外申请im:resource的读取权限但本文只聚焦发送所以这两个够了。接下来是 Node.js 脚本。飞书 API 的鉴权走 tenant_access_token你需要用 app_id 和 app_secret 换取token 有效期约两小时建议缓存。下面是一个完整的发送文件脚本包含 token 获取、文件上传、消息创建三步// feishu-send-file.js import fs from fs; import path from path; import fetch from node-fetch; import FormData from form-data; const APP_ID process.env.FEISHU_APP_ID; const APP_SECRET process.env.FEISHU_APP_SECRET; const TARGET process.env.FEISHU_BOT_TARGET; let cachedToken null; let tokenExpireAt 0; async function getTenantToken() { const now Date.now(); if (cachedToken now tokenExpireAt) return cachedToken; const res await fetch( https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET }) } ); const data await res.json(); if (data.code ! 0) throw new Error(token failed: ${data.msg}); cachedToken data.tenant_access_token; tokenExpireAt now (data.expire - 60) * 1000; return cachedToken; } async function uploadFile(filePath) { const token await getTenantToken(); const form new FormData(); form.append(file_type, stream); form.append(file_name, path.basename(filePath)); form.append(file, fs.createReadStream(filePath)); const res await fetch( https://open.feishu.cn/open-apis/im/v1/files, { method: POST, headers: { Authorization: Bearer ${token}, ...form.getHeaders() }, body: form } ); const data await res.json(); if (data.code ! 0) throw new Error(upload failed: ${data.msg}); return data.data.file_key; } async function sendFileMessage(fileKey, fileName) { const token await getTenantToken(); const res await fetch( https://open.feishu.cn/open-apis/im/v1/messages?receive_id_typeopen_id, { method: POST, headers: { Authorization: Bearer ${token}, Content-Type: application/json }, body: JSON.stringify({ receive_id: TARGET, msg_type: file, content: JSON.stringify({ file_key: fileKey }) }) } ); const data await res.json(); if (data.code ! 0) throw new Error(send failed: ${data.msg}); return data.data.message_id; } const filePath process.argv[2]; if (!filePath) { console.error(usage: node feishu-send-file.js file); process.exit(1); } const key await uploadFile(filePath); const msgId await sendFileMessage(key, path.basename(filePath)); console.log(sent:, msgId);运行方式node feishu-send-file.js ./report.pdf。脚本会依次打印上传和发送的结果。注意receive_id_typeopen_id要和receive_id的类型匹配如果你发给群组改成chat_id并把 TARGET 换成oc_开头的 ID。如果你更想用 OpenClaw 的命令行方式等价操作是openclaw message send \ --channel feishu \ --target user:ou_xxxxxxxx \ --message 报告已生成请查收 \ --media ./report.pdfOpenClaw 内部会走上面同样的两步流程只是把 token 管理和 form-data 封装掉了。两种方式选一种即可Node.js 脚本适合需要条件判断、批量处理的场景OpenClaw 命令适合快速验证和简单推送。4. 验证请求与成功结果一次真实发送的完整动作配置写完了必须跑一次真实发送才能确认链路通。我建议用一个 1MB 以内的 PDF 或 ZIP 做首次验证文件太大容易把网络问题和权限问题混在一起。第一步确认环境变量已加载。在 Node.js 里可以用console.log(process.env.FEISHU_APP_ID)快速检查但别把 secret 打出来。如果用的是 dotenv记得在脚本顶部import dotenv/config。第二步执行发送脚本。观察控制台输出正常情况会先打印sent: om_xxxxxxxx其中om_开头的是消息 ID。如果卡在 token 获取阶段通常是 app_id 或 app_secret 错了如果卡在上传阶段看错误信息里有没有permission字样。第三步打开飞书找到机器人对话或目标群组。你应该能看到一条带文件卡片的消息显示文件名和大小点击即可下载。如果消息发出去了但文件显示「已过期」或无法下载多半是 file_key 对应的资源被清理了重新上传即可。第四步用飞书的返回体做二次确认。在sendFileMessage里把data完整打印出来正常返回结构包含message_id、chat_id、create_time等字段。你可以把这些字段记下来后续做消息追踪或撤回时用得上。一个容易被忽略的验证点是机器人是否真的在目标会话里。如果机器人没有被拉进群组或者用户没有和机器人建立会话发送会返回receive_id invalid。解决方法是先在飞书里手动给机器人发一条消息建立会话关系再跑脚本。实测下来从零到收到文件顺利的话 10 分钟内能完成。卡点主要集中在权限发布和 receive_id 类型匹配上。把这两点确认好后面的批量发送就是复制粘贴的事。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 报错对照报错是绕不开的关键是要能快速定位是哪一层的问题。下面按真实遇到的频率排列给出错误信息、原因和解决动作。401 UnauthorizedTaoToken 侧如果你在验证 TaoToken 通道时看到 401先检查Authorization头是不是Bearer加 Key注意 Bearer 后面有一个空格。其次检查 Key 是否被禁用或额度耗尽去控制台的 API Keys 页面看状态。还有一种情况是 Base URL 写成了https://taotoken.net/api/带尾斜杠某些 HTTP 客户端会把路径拼成//v1/models导致鉴权失败。local proxy failed这个报错通常出现在你本地网络环境有代理设置、但代理不可用时。Node.js 的 fetch 会读取HTTP_PROXY/HTTPS_PROXY环境变量如果这些变量指向一个已经关闭的本地端口就会报 local proxy failed。解决方法是检查环境变量或者在脚本里显式设置process.env.NO_PROXY *绕过代理。注意这里说的是本地开发环境的网络配置问题不涉及任何跨境网络工具。reading choices of undefined这个报错来自模型响应解析。当你调用 TaoToken 的对话接口返回体里没有choices字段时说明请求本身失败了但代码直接去读data.choices[0]。正确做法是先判断res.ok和data.error再取 choices。常见触发原因是 Model ID 填错比如把claude-3-5-sonnet写成了claude-3.5-sonnet接口返回错误对象而不是正常响应。OAuth 相关报错如果你在配置 Claude Code 或类似工具时看到 OAuth 失败检查是不是把 API Key 模式和 OAuth 模式混用了。TaoToken 的接入文档里对这两种模式有区分API Key 模式直接填 Key 即可不需要走 OAuth 授权流程。把配置里的 auth 类型改对问题通常就消失了。飞书侧 file_key invalid上传返回了 file_key但发送时报无效。检查上传时file_type是否填的stream以及发送时msg_type是否填的file。两者必须匹配图片要用image类型和msg_type: image混用会失败。权限不足 im:resource错误信息里明确提到 scope 缺失。回到飞书开放平台确认im:resource已开通并发布了新版本。权限变更后需要重新获取 tenant_access_token旧 token 不会自动带上新权限。把这几类报错对照着排查大部分问题能在几分钟内解决。建议在脚本里加一层错误日志把飞书返回的code和msg完整打出来比只看 HTTP 状态码有用得多。6. 语义一致 CTA把 Key 配置与接入文档放在手边整条链路跑通后你会发现真正花时间的不是写代码而是配置和排障。把 TaoToken 的 Key 管理和接入文档放在顺手的位置下次换项目或加新通道时能省不少事。需要创建或轮换 API Key 时直接进控制台 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Base URL、Model ID 列表和各工具的配置示例。如果你要验证模型对话是否正常可以用模型对话入口 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 。长期做编码和 Agent 任务的话Coding Plan 页面在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。飞书侧的权限和 API 文档在开放平台里建议把「权限管理」和「API 调试台」两个页面收藏。调试台可以直接发请求不用写代码就能验证 file_key 和消息发送排障时非常省时间。最后给一个实用技巧把飞书发送封装成一个可复用的函数参数只留文件路径和目标 IDtoken 缓存和错误重试都在函数内部处理。这样你的业务代码里只需要一行调用后续换通道或加日志也不用改业务逻辑。文件推送这件事跑通一次之后就是纯粹的工程化问题了。
阅读完成 · 觉得有帮助?