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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。注意这两个地址的用途不同:官网用于注册、查看文档、管理 Key;API 基址用于代码里的 endpoint 配置。你需要在官网注册后,进入控制台创建 API Key,这个 Key 就是后续所有请求的凭证。
具体操作路径:打开官网,登录后进入控制台页面(deep link 为 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ),在 API Keys 管理页( https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite )创建一个新的 Key。创建时建议按用途命名,比如openclaw-feishu-bot,方便后续排查。Key 只显示一次,复制后立刻存进你的密钥管理工具或本地 .env。
如果你用的是 Claude Code 这类编码工具,TaoToken 也提供了对应的接入文档( https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ),里面会说明 Base URL 和 Model ID 怎么填。对于 OpenClaw 场景,你主要关心的是:当 OpenClaw 需要调用模型来生成文件内容或处理文本时,它的模型请求应该指向 TaoToken 的 API 基址,而不是散落到各个厂商的原生地址。
这里给一个 Node.js 项目里常见的 .env 配置片段,把飞书凭证和 TaoToken Key 放在一起管理:
# .env FEISHU_APP_ID=cli_xxxxxxxxxxxx FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx FEISHU_BOT_TARGET=ou_xxxxxxxxxxxxxxxxxxxxxxxx TAOTOKEN_API_BASE=https://taotoken.net/api TAOTOKEN_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx TAOTOKEN_MODEL_ID=claude-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_type=open_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_type=open_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 Unauthorized(TaoToken 侧):如果你在验证 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_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Base URL、Model ID 列表和各工具的配置示例。如果你要验证模型对话是否正常,可以用模型对话入口: https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。长期做编码和 Agent 任务的话,Coding Plan 页面在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。
飞书侧的权限和 API 文档在开放平台里,建议把「权限管理」和「API 调试台」两个页面收藏。调试台可以直接发请求,不用写代码就能验证 file_key 和消息发送,排障时非常省时间。
最后给一个实用技巧:把飞书发送封装成一个可复用的函数,参数只留文件路径和目标 ID,token 缓存和错误重试都在函数内部处理。这样你的业务代码里只需要一行调用,后续换通道或加日志也不用改业务逻辑。文件推送这件事,跑通一次之后就是纯粹的工程化问题了。