- Published on
对接飞书多维表格 | Feishu Bitable Integration: Webhook Sync & API Operations
- Authors
- Name
- Shelton Ma
1. 使用飞书多维表格「自动化」将新增数据同步至系统
虽然可以通过定时轮询飞书多维表格 API 实现数据同步,但如果新增记录属于低频操作,不推荐使用轮询方式。
操作步骤如下:
- 系统端创建 Webhook 接口,用于接收飞书的回调请求。
- 在飞书多维表格中配置「自动化」流程:当用户新增记录时,触发调用上述 Webhook,实现系统端的数据写入。
2. 将多维表格的变更事件推送至系统
如果系统部署在公网环境中,可直接通过公开的 Webhook API 接收飞书的变更事件。
若系统未暴露在公网,可采取替代方案:
- 在飞书多维表格中设置变更事件发送至某个群聊。
- 系统侧通过运行 WebSocket 客户端监听该群聊中的机器人通知,从而感知并处理数据变更事件。
1. 基本文档说明
使用长连接接收事件
2.与开放平台建立一条 WebSocket
全双工通道。后续当应用订阅的事件发生时,开放平台会通过该通道向你的服务器发送事件消息。无需提供外网可访问的服务
3. 总配置流程
飞书云文档事件
支持不太好, 通过 自动化
将变更发送到飞书群聊, 自建应用监听飞书群聊消息, 再通过系统调用多维表格 API 进行数据读写
创建应用
- 应用中添加
机器人
能力 - 在权限管理中添加应用身份权限:
查看、评论、编辑和管理多维表格 bitable:app
im:message.group_msg
和im:message
读取群聊消息 - 在事件与回调中, 订阅方式使用
长连接
接收事件, 至少订阅drive.file.bitable_record_changed_v1
im.message.receive_v1
- 记录
App ID
和App Secret
, 获取tenant_access_token - 服务端 API - 开发文档 - 飞书开放平台
- 应用中添加
创建多维表格, 需要添加创建的应用
... > ...More > Add Applications
,确保应用有权限调用多维表格 API。配置多维表格自动化, 触发动作为
发送飞书消息到群聊
, 同时发送人要是用户, 而不是应用或表格助手机器人发送的消息不会触发接收事件(事件只推送用户发送的消息)
, 这是飞书开放平台的主动消息防循环机制:- 防消息循环: 避免机器人发送消息后触发新事件,导致无限循环
- 权限隔离: 机器人只能接收用户或其他机器人发送的消息(需开通
im:message.group_msg
权限) - 事件溯源: 所有事件均需明确来源于用户操作
不必要(已经不再监听云文档变动, 而是通过自动化配置发送消息到群聊): 给多维表格订阅云文档事件
在本地启动websocket客户端, 并配置接收事件的回调函数, 将可以处理事件(3s内处理结束)
import * as Lark from "@larksuiteoapi/node-sdk"; const baseConfig = { appId: "xxx", appSecret: "xxx", }; const client = new Lark.Client(baseConfig); const wsClient = new Lark.WSClient({ ...baseConfig, loggerLevel: Lark.LoggerLevel.debug, }); wsClient.start({ // 处理「多维表格记录变更」事件,事件类型为 drive.file.bitable_record_changed_v1 eventDispatcher: new Lark.EventDispatcher({}) .register({ "drive.file.bitable_record_changed_v1": async (data) => { console.log(data); // 示例操作:接收消息后,调用「发送消息」API 进行消息回复。 }, }) });
3. 使用 飞书 SDK 对多维表格进行数据读写
1. 创建自建飞书应用
确保应用身份权限: 查看、评论、编辑和管理多维表格 bitable:app
获取多维表格参数: app_token(obj_token)
2. 3. 更新记录数据
所需参数:
app_token
: 多维表格obj_token
table_id
: 来自多维表格url中, 表示访问的数据表idrecord_id
: 当前记录id, 当新创建记录时调用系统的webhook API
时, 会携带fields
: 传入需更新的字段
4. 执行封装实现(不推荐)
import axios from "axios";
import keys from "../../configBridge";
interface FeishuConfig {
app_id: string;
app_secret: string;
}
interface TokenInfo {
token: string;
expiresAt: number; // Unix 时间戳(毫秒)
}
export class Feishu {
private config: FeishuConfig;
private tenantTokenInfo: TokenInfo | null = null;
constructor() {
this.config = {
app_id: keys.feishu.FEISHU_APP_ID || "",
app_secret: keys.feishu.FEISHU_APP_SECRET || "",
};
}
// 判断 token 是否过期
private isTokenExpired(tokenInfo: TokenInfo | null): boolean {
if (!tokenInfo) return true;
return Date.now() >= tokenInfo.expiresAt;
}
// 获取 tenant_access_token
async getTenantAccessToken() {
if (!this.isTokenExpired(this.tenantTokenInfo)) {
return this.tenantTokenInfo!.token;
}
const response = await axios.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
{
app_id: this.config.app_id,
app_secret: this.config.app_secret,
}
);
const data = response.data;
if (data.code !== 0 || !data.tenant_access_token) {
throw new Error(`获取 tenant_access_token 失败: ${data.msg}`);
}
const expiresIn = data.expire; // 秒
this.tenantTokenInfo = {
token: data.tenant_access_token,
expiresAt: Date.now() + expiresIn * 1000 - 60_000, // 提前 60 秒过期
};
return this.tenantTokenInfo?.token;
}
// 获取 app_token (Bitable 应用级 token)
async getAppToken(baseToken: string): Promise<string> {
return baseToken;
}
// 更新记录
async updateRecord(params: {
appToken: string;
tableId: string;
recordId: string;
fields: Record<string, any>;
}): Promise<any> {
const token = await this.getTenantAccessToken();
const url = `https://open.feishu.cn/open-apis/bitable/v1/apps/${params.appToken}/tables/${params.tableId}/records/${params.recordId}`;
const response = await axios.put(
url,
{
fields: params.fields,
},
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
if (response.data.code !== 0) {
throw new Error(`更新记录失败: ${response.data.msg}`);
}
return response.data;
}
}