简介:这套飞书多维表格 OpenClaw 技能包面向希望用低代码方式搭建业务应用的非技术用户与团队管理者,将创建、读取、更新、删除等 CRUD 操作封装为可一键安装的模板,解决从零搭表、重复录入与协作管理效率低的问题。包体共 13 个文件、压缩后仅 55KB:以 md 文档为主体,涵盖字段类型映射、自动化工作流、权限指南与公式参考,配合 Python 脚本及 JSON 配置完成核心操作,另含安装脚本与辅助文件便于快速部署。目前已有 90 人学习下载。实际使用中,无需复杂编程即可按项目管理、客户维护、库存跟踪等场景灵活调整模板,既适合刚接触 OpenClaw 技能的入门读者,也能为需要扩展自定义流程的进阶用户提供可复用脚本与排错思路,是一份轻量实用的技能模板组合。
1. 从「打开表格手动改」到「一句话跑通 CRUD」:飞书多维表格 OpenClaw 技能到底解决什么
团队把客户、任务、库存都收进飞书多维表格之后,真正的麻烦才刚开始:每天大量的「查一下、改一下、删一条」操作,要么打开表格手动找行,要么写个用完就扔的临时脚本。这个技能包要做的,是把飞书多维表格的日常 CRUD 封装成 OpenClaw 能调用的技能——你直接用自然语言说「把预算表里超支的三条标记一下」「查一下本周到期的合同」,OpenClaw 解析意图后调用脚本,脚本直连多维表格 API 执行增删改查,再把结构化结果翻译回人能读的话。适合手里有三五个表格天天要维护、又不想给每个人都开编辑权限的运营、项目与售前团队。
2. 动手前先摸清两头:OpenClaw 技能触发机制与多维表格 API 关键参数
2.1 OpenClaw 的「技能」不是机器人插件,而是一个带描述的脚本目录
很多人把 OpenClaw 的技能当成黑匣子,其实拆开看就是一个「带说明书的脚本目录」。常见做法是:一个技能目录里放一个描述文件(SKILL.md 或 skill.yaml)、一个可执行脚本、一份依赖声明。描述文件决定了框架在什么场景下调用它,脚本负责干实事。它和你熟悉的「插件」有个明显差异:插件的边界是功能,技能的边界是意图。比如同样一个「查合同」技能,你问「这个月到期的合同有哪些」和「把张三那份合同金额调成 20 万」,框架会根据描述文件里的语义指引,把请求路由到同一个技能、但不同 action 上。
这个机制对飞书多维表格特别合适,因为表格操作天然是「动词 + 对象 + 条件」结构:查、建、改、删是动词,表名是对象,筛选条件是修饰语。把这一层拆成参数,技能就活了。我一般会让描述文件把参数写得尽量细,比如注明「table 必须是 config.json 里 datasources 中已配置的表名」,避免模型猜出一个不存在的表。另外一个容易被忽略的点:OpenClaw 跑技能脚本时,走的是 HTTP API,和你用云端模型还是本地模型无关。就算你切了本地小模型,技能照样能跑,因为模型只负责把自然语言翻译成参数,真正干活的是脚本。
2.2 多维表格 API 五件套:tenant_access_token、app_token、table_id、record_id 与字段类型
写代码之前,先记住飞书多维表格 API 的四个核心标识,少一个都调不通。第一个是 tenant_access_token,应用级凭证,所有业务请求都要带它做鉴权;第二个是 app_token,定位到具体某一个多维表格;第三个是 table_id,定位到表格里的某一张数据表;第四个是 record_id,定位到表里的某一行记录。前三个找起来其实不用查文档:打开任意一个多维表格,地址栏长这样https://xxx.feishu.cn/base/Abc123DEF?table=Tbl456&view=vew789,/base/后面那段就是 app_token,?table=后面那段就是 table_id。record_id 则要靠查询接口返回。
第五个概念是字段类型。多维表格的列为文本、数字、单选、多选、日期、人员等类型,API 传值时格式各不相同:数字字段直接传数值,单选字段传选项文本,多选字段传字符串数组,日期字段传毫秒时间戳或「2024-01-01」这类格式化字符串。这个设计看似简单,实际最容易翻车,后面避坑章节会专门展开。创建或更新记录时,payload 是一个fields对象,key 是字段名,value 按字段类型决定,例如{"客户名称": "某某科技", "合同金额": 120000, "标签": ["重点", "新签"]}。
2.3 技能包目录结构:zip 解压之后你该看到哪些文件
既然标题带「一键安装.zip」,技能包的结构就得从一开始就定好,不然后面交付时手忙脚乱。我一般会按下面的目录组织:
feishu-bitable-skill/ ├── SKILL.md # 技能描述,给 OpenClaw 读 ├── skill.py # 主脚本,CRUD 全在这里 ├── config.example.json # 配置模板,复制成 config.json 后填 ├── requirements.txt # 依赖声明,目前只需要 requests └── install.sh # 一键安装脚本,做依赖检查和目录部署SKILL.md 是 OpenClaw 判断「什么时候该用这个技能」的依据,内容质量直接影响触发准确率。skill.py 是核心实现,建议把所有动作收敛成query / create / update / delete四个子命令,方便框架按参数调用。config.example.json 放 app_id、app_secret 和表名映射,这样用户安装时不需要改任何代码。install.sh 做三件事:检查 Python 版本、安装 requests、把整个目录链接到 OpenClaw 的技能目录。在写技能代码之前先把这个结构立起来,后面每写一段代码都知道该放哪。
3. 从零跑通「查询记录」:拿 token、翻页、注册成技能的完整链路
3.1 在飞书开发者后台创建自建应用,并开通多维表格读写权限
第一步是去飞书开发者后台创建一个企业自建应用,拿到app_id和app_secret。路径是「开发者后台 → 创建应用 → 企业自建应用」,创建完成后在「凭证与基础信息」页面能看到这两个值。注意:app_secret 只在创建时完整展示一次,后面再进页面只能重置,建议拿到就先存进自己的密码管理器。
接着去「权限管理」里开通多维表格相关权限。搜索框里搜「多维表格」或「bitable」,把读写相关权限勾上,然后一定要在「版本管理与发布」里创建一个版本并发布。很多第一次做的人在这里卡住:权限勾了但没发布版本,token 能拿到但接口一直报权限不足。发布后回到多维表格页面,点右上角「...」→「添加文档协作者」,在搜索框里输入你刚创建的应用名称,把它加为表格协作者。这一步最容易被漏:应用有 API 权限,不等于它能访问某张具体表格,你必须把它加到协作者列表里。
提示:自建应用本质是一个「机器人身份」,不是某个用户的身份。所以它的权限 = 后台勾选的权限 + 被添加为协作者的文档范围,两者缺一不可。
准备工作做完,写一个最小的 token 获取脚本验证链路:
# get_token.py import os import requests APP_ID = os.getenv("FEISHU_APP_ID", "") APP_SECRET = os.getenv("FEISHU_APP_SECRET", "") BASE = "https://open.feishu.cn/open-apis" def get_tenant_token(): """获取 tenant_access_token,有效期约 2 小时""" resp = requests.post( f"{BASE}/auth/v3/tenant_access_token/internal", json={"app_id": APP_ID, "app_secret": APP_SECRET}, timeout=10, ) data = resp.json() if data.get("code") != 0: raise RuntimeError(f"token 获取失败: {data}") return data["tenant_access_token"] if __name__ == "__main__": print(get_tenant_token())这段代码里唯一需要解释的是接口路径:/auth/v3/tenant_access_token/internal是「企业自建应用」专用,拿到的是应用身份令牌,不是用户身份令牌。响应里的code为 0 表示成功,非 0 时不要只看 code,把整个 data 打出来,里面会明确提示是 app_id 错误还是 secret 错误。token 默认两小时过期,技能脚本里每次执行都重新获取,不要存缓存,省得处理过期逻辑。
3.2 第一个能跑的查询:用 Python 列出一条记录
token 能拿到,接下来写查询。查询是 CRUD 里最安全的动作,也最适合用来验证 app_token 和 table_id 是否正确。先写一个只查一页数据的函数:
# query_records.py import os import requests BASE = "https://open.feishu.cn/open-apis" def get_tenant_token(): # ... 同上文的 token 获取逻辑 ... def query_records(app_token, table_id, page_size=100, page_token=None): """查询多维表格记录,返回 (records, has_more, next_page_token)""" token = get_tenant_token() url = f"{BASE}/bitable/v1/apps/{app_token}/tables/{table_id}/records" headers = {"Authorization": f"Bearer {token}"} params = {"page_size": page_size} if page_token: params["page_token"] = page_token resp = requests.get(url, headers=headers, params=params, timeout=10) data = resp.json() if data.get("code") != 0: raise RuntimeError(f"查询失败: {data}") records = data["data"] return ( records.get("items", []), records.get("has_more", False), records.get("page_token", None), ) if __name__ == "__main__": app_token = input("app_token: ").strip() table_id = input("table_id: ").strip() items, has_more, _ = query_records(app_token, table_id, page_size=10) print(f"返回 {len(items)} 条,还有更多: {has_more}") for item in items[:3]: print(item["record_id"], item["fields"])注意headers里Authorization的值必须是Bearer加 token,中间有一个空格,拼错会报鉴权失败。page_size最大可设到 500,示例里先用 10 验证链路。返回体里items是记录数组,每条记录包含record_id和fields两个字段——record_id是后续更新和删除的唯一依据,fields就是这行数据的列值映射。
跑通这一步,说明你的应用身份、表格访问权限、接口路径全部正确。如果返回空列表,先别怀疑代码,回到表格里确认有没有数据,或者看看 app_token 是不是从分享链接里复制的——分享链接里的串经常不是真正的 app_token,这个坑第 5 章会细说。
3.3 用 SKILL.md 声明触发条件,让 OpenClaw 在对话中认出这个技能
脚本能跑了,但 OpenClaw 怎么知道该在什么时候调用它?这就靠 SKILL.md。我一般用 Markdown 带 front-matter 的写法,把技能名、用途描述、参数说明放在最前面,让框架做意图匹配时能快速命中:
--- name: feishu-bitable description: 查询、创建、更新、删除飞书多维表格中的记录。当用户提到查数据、看任务、改状态、新增一行、删记录、表格里有什么时使用。 arguments: action: type: string enum: [query, create, update, delete] description: 要执行的操作 table: type: string description: 表名,必须是 config.json 里 datasources 中已配置的名称 record_id: type: string description: 记录 ID,update 和 delete 时使用 fields: type: object description: 字段名到值的映射,create 和 update 时使用 --- # 飞书多维表格 CRUD 技能 操作飞书多维表格的查询、创建、更新、删除。 执行方式:`python skill.py <action> --table <表名> [--record <record_id>] [--json '<fields>']`description 里要把触发场景写具体,尤其是「当用户提到……时使用」这一段。写得太泛,模型什么请求都往这里发;写得太窄,该触发时不触发。我自己的习惯是覆盖高频说法:查数据、看任务、改状态、新增一行、删掉这条记录。称呼上「表格」「多维表格」「数据表」都提一句,模型对同义词的把握没你想的那么稳。arguments 部分声明了四个参数,其中action用枚举限定,防止模型编造出不存在的操作。
到这里,最小链路已经成立:用户在 OpenClaw 里说一句话 → 框架匹配到 feishu-bitable 技能 → 按 arguments 提取参数 → 执行python skill.py query --table 项目表→ 脚本连飞书 API 取数 → 结果返回给模型组织成自然语言回答。先把这个闭环跑通,再往下补完整的增删改。
4. 把日常 CRUD 补全:创建、更新、删除的接口与命令行参数设计
4.1 创建记录:单条 POST 与批量 batch_create
创建记录是 CRUD 里最常用的写操作。单条创建的接口是POST /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records,请求体就是{"fields": {...}}。实现起来非常简单:
# skill.py 中的创建函数 import json import requests def create_record(app_token, table_id, fields): """创建一条记录,返回新记录的 record_id""" token = get_tenant_token() url = f"{BASE}/bitable/v1/apps/{app_token}/tables/{table_id}/records" headers = {"Authorization": f"Bearer {token}"} resp = requests.post(url, headers=headers, json={"fields": fields}, timeout=10) data = resp.json() if data.get("code") != 0: raise RuntimeError(f"创建失败: {data}") return data["data"]["record"]调用时传入的fields必须和表结构对齐。文本字段传字符串,数字字段传数值,单选字段传选项文本,多选字段传字符串数组。例如往「任务表」加一行:
new_record = create_record( app_token, table_id, { "标题": "部署飞书技能到生产环境", "优先级": "高", # 单选 "预估工时": 4, # 数字 "标签": ["运维", "自动化"] # 多选 } ) print(new_record["record_id"])一次性创建多条时用批量接口POST .../records/batch_create,请求体是{"records": [{"fields": {...}}, {"fields": {...}}]}。批量接口的意义不只是少写几个请求,而是保证一批数据要么都成功、要么都失败,避免数据半落不落。批量创建单次上限 100 条,超过要分批。真要导入几千条旧数据,建议分批加间隔,否则容易触发接口限流。
4.2 更新记录:先查 record_id 再精准更新
更新要先拿到 record_id。最直接的方式是用户在对话里把某条记录的特征告诉你,但模型通常只能给出「把标题为 X 的记录改成 Y」这种条件,给不出 record_id。所以技能里要有一个「先查后改」的流程:先用查询接口按条件找到 record_id,再调更新接口。
def update_by_condition(app_token, table_id, condition_field, condition_value, new_fields): """按条件匹配第一条记录并更新""" items, has_more, _ = query_records(app_token, table_id, page_size=100) target = None for item in items: if str(item["fields"].get(condition_field)) == str(condition_value): target = item break if not target: raise RuntimeError(f"没有找到 {condition_field} = {condition_value} 的记录") return update_record(app_token, table_id, target["record_id"], new_fields)这段逻辑里有两个实用细节。第一,匹配时用str()包裹两边再比较,因为同一列里可能混着数字和字符串,比如「状态」列有的行是文本「已完成」,有的行因为历史原因存成了数字 1,直接比较会漏匹配。第二,这个实现只匹配第一条,如果表格里有重复记录,建议在提示词层面让模型补充更多条件,或者改用 view_id 过滤,而不是在代码里做复杂去重。更新接口本身很简单:PUT .../records/{record_id},请求体同创建,传{"fields": {...}},只写要改的字段即可,未传字段保持不变。这个「部分更新」语义很关键,不用担心覆盖掉其他列。
4.3 删除记录:DELETE 接口与批量删除的安全闸门
删除是 CRUD 里唯一不可逆的操作,代码实现简单,但必须在两个层面加防护。第一层在我的技能脚本里加确认参数,第二层在 OpenClaw 的提示词层面要求模型必须拿到用户明确确认后才能删。
def delete_record(app_token, table_id, record_id, confirm=False): """删除单条记录,confirm=False 时拒绝执行""" if not confirm: raise RuntimeError("删除操作需要 confirm 参数,请先让用户确认") token = get_tenant_token() url = f"{BASE}/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}" headers = {"Authorization": f"Bearer {token}"} resp = requests.delete(url, headers=headers, timeout=10) data = resp.json() if data.get("code") != 0: raise RuntimeError(f"删除失败: {data}") return data.get("data", {})批量删除走POST .../records/batch_delete,请求体是{"records": ["rec_id1", "rec_id2"]},同样有 100 条上限。我建议批量删除函数强制要求传入至少一个confirm_reason字段,不填就不执行。这个字段会被记录在日志里,出问题的时候能追溯是谁、为什么删了这一批。实际经验是:删除相关的翻车事故,九成不是接口调不通,而是没确认就删了、或者删错了筛选条件命中的行。这类事故在项目周报里非常难看,多一道确认成本极低。
4.4 把四个动作收进一个入口:action 参数与表名映射设计
四个函数都齐了,需要把它们收进同一个命令行入口,方便 OpenClaw 用统一方式调用,也方便你手动调试。用 argparse 实现一个带子命令风格的主入口:
# skill.py 的主入口 import argparse import json import sys def main(): parser = argparse.ArgumentParser(description="飞书多维表格 CRUD 技能") parser.add_argument("action", choices=["query", "create", "update", "delete"]) parser.add_argument("--table", required=True, help="数据表名称,对应 config.json 中的 datasources") parser.add_argument("--record", help="record_id,update/delete 时使用") parser.add_argument("--json", dest="fields_json", help="字段 JSON,如 '{\"标题\": \"新任务\"}'") parser.add_argument("--confirm", action="store_true", help="删除操作必须携带") parser.add_argument("--condition", help="按条件更新/删除,格式 '字段名=值'") args = parser.parse_args() if args.fields_json: fields = json.loads(args.fields_json) else: fields = {} # 根据 action 分发到对应的 CRUD 函数 if args.action == "query": print(query_records(...)) elif args.action == "create": print(create_record(...)) elif args.action == "update": if args.record: print(update_record(..., args.record, fields)) elif args.condition: field, value = args.condition.split("=", 1) print(update_by_condition(..., field, value, fields)) elif args.action == "delete": if not args.confirm: sys.exit("删除需要 --confirm 参数") print(delete_record(..., args.record, confirm=True)) if __name__ == "__main__": main()这个入口设计有三个值得留意的点。第一,--table接收的是表名而不是 table_id,真正的映射关系放在 config.json 里,这样模型的参数提取压力小很多。第二,--condition支持「字段名=值」这种极简格式,让模型在不知道 record_id 时也能完成「把状态为进行中的任务改成已完成」这类操作。第三,所有输出建议用json.dumps序列化,保证 OpenClaw 解析结果时不会因为中文字符或引号问题翻车。config.json 的表名映射长这样:
{ "app_id": "cli_xxxxx", "app_secret": "xxxxx", "datasources": { "项目表": {"app_token": "Abc123DEF", "table_id": "tblAAA"}, "任务表": {"app_token": "Abc123DEF", "table_id": "tblBBB"} } }5. 避坑与排查:飞书多维表格技能最容易翻车的 5 个细节
5.1 从分享链接复制 token,接口一直报「app not found」
现象:查询接口返回code: 1256之类错误,提示找不到应用或表格,但 URL 看着没问题。原因:很多人习惯点表格右上角的「分享」,从分享链接里复制一串字符当作 app_token。分享链接通常带的是短链标识或带额外参数,和开放平台 API 用的 app_token 完全不是一回事。解决:回到浏览器地址栏,从完整的页面 URL 里取。格式是https://xxx.feishu.cn/base/真实app_token?table=真实table_id&view=vewxxx,/base/后到第一个?之间的串才是 app_token。我现在的习惯是拿到 URL 先复制到记事本,按?切分成三段,再分别填进 config.json,从不在代码里临时粘贴。
5.2 往数字字段塞了字符串,写入被字段校验拦下
现象:创建记录时接口返回字段校验错误,比如「字段类型不匹配」或「无法识别字段类型」。原因:模型从自然语言里提取参数时,把「金额 120000」提取成了字符串"120000",而多维表格里这一列是数字类型,类型对不上直接拒绝写入。更隐蔽的是日期字段,传"2024-01-01"可能侥幸通过,传"2024/01/01"就会报错,因为接口对日期格式有严格约定。解决:在create和update的分发逻辑里加一个normalize_fields函数,根据每个表已知的字段类型做转换。简单做法是维护一份「字段类型映射」放 config.json 里;偷懒做法是在脚本里用isinstance判断外部传入值,数字字符串能转 int 就转 int。字段多的时候别偷懒,类型映射表值得维护。
5.3 token 有效但查不到数据:应用没被加为表格协作者
现象:token 获取成功,http 状态码也正常,但返回的 items 是空数组,或者直接提示「没有权限访问该文档」。原因:自建应用的 API 权限是「能力」,能不能访问某张具体表格是「范围」。后台开了多维表格读写权限,只代表应用有这个能力,不代表它自动能读你所有的表。每张表都得手动把应用添加为协作者。解决:打开目标多维表格,点右上角「...」→「添加文档协作者」,搜应用名称(不是应用描述),授予「可编辑」权限。如果是新创建的测试表,检查一下协作者列表里有没有这个应用。这个问题特征明显但也迷惑性强,尤其当你有多个多维表格时,容易误判是代码问题。
5.4 查询只回 20 条就以为数据丢了:忘翻页
现象:接口返回成功,但记录只有一页,表格里明明有几百行,query 结果却缺了一大半,造成「数据丢了」的误判。原因:多维表格查询接口默认page_size是 20 或 100(取决于接口版本),且单页返回后has_more为 true 时需要用page_token继续拉下一页。脚本里只取了第一页,没有翻页逻辑。解决:把page_size显式设为 100 或 500,然后循环拉取直到has_more为 false。我一般封装一个query_all_records函数,内部用while has_more:累加结果,这样技能在对话里回答「一共有多少条」时不会给出偏小的数字。
5.5 批量操作一次超过 100 条被拒:分批重试
现象:批量创建或删除时接口返回code: 99991400之类的参数错误,或者提示超出数量限制。原因:多维表格批量接口单次限制 100 条,超过上限请求直接被拒。写成循环逐条调又太慢,1000 条数据要跑 10 次请求,还容易中途触发限流。解决:写一个chunked_batch包装函数,把记录列表按 100 条切片,逐批调用后再汇总结果。每批之间加 200~500 毫秒延时,避免触发频率限制。给技能做数据迁移时,这个切片逻辑几乎是必备的;不加的话,数据一多就翻车。
6. 交付成「一键安装.zip」:目录规范、安装脚本与自测清单
6.1 zip 包里的标准目录与安装脚本做了什么
交付时把整个feishu-bitable-skill/目录打成一个 zip,命名带上版本号,比如feishu-bitable-skill-v1.0.0.zip。解压后用户只会做三件事:复制config.example.json为config.json、填入自己的 app_id 和 app_secret、运行bash install.sh。install.sh 的核心逻辑是往 OpenClaw 技能目录里放一个指向本项目的链接或直接拷贝,并检查 Python 版本和 requests 依赖:
#!/usr/bin/env bash # install.sh 一键安装脚本 set -e # 1. 检查 Python 版本 python3 -c "import sys; assert sys.version_info >= (3,8), '需要 Python 3.8+'" # 2. 安装依赖 pip3 install -r requirements.txt # 3. 检查配置文件 if [ ! -f config.json ]; then cp config.example.json config.json echo "请编辑 config.json,填入 app_id 和 app_secret" exit 1 fi # 4. 复制到 OpenClaw 技能目录并做安装后验证 SKILL_DIR="$HOME/.openclaw/skills/feishu-bitable" mkdir -p "$SKILL_DIR" cp skill.py SKILL.md config.json "$SKILL_DIR/" echo "安装完成,可用下面命令自测:" echo "python3 skill.py query --table 任务表"这段脚本的逻辑按顺序走:先确认 Python 版本,避免用户用 2.x 或太旧的 3.x 跑出诡异错误;再装依赖;然后检查 config.json 是否存在,不存在就复制模板并退出,让用户先填配置;最后把三个关键文件复制到 OpenClaw 的默认技能目录。set -e表示任何一步失败就立即终止,不带着半残的环境继续。如果你在安卓端跑 OpenClaw,目录路径会不太一样,把$HOME/.openclaw/skills改成你实际挂载的技能目录即可,脚本结构不用变。
6.2 三步端到端自测清单
装完之后我习惯按三步验收一个技能包,任何一步没过都不算交付完成。第一步是命令行自测:python3 skill.py query --table 任务表,能打印出记录就算脚本没问题。第二步是自然语言自测:在 OpenClaw 对话里问「任务表里现在有几条记录」,看模型能否正确路由到技能并返回数字。第三步是错误注入自测:故意传一个不存在的表名,看脚本报错是否足够清晰,而不是抛一个 Python traceback 给用户。三步全过后,这个 zip 才敢往外发。我自己每次改完 skill.py,都会先跑一遍命令行再交给对话层,这个习惯帮我挡掉了至少一半的线上事故。希望帮到你。
本文还有配套的精品资源,点击获取